Project 10: I2C Driver for OLED Display

Build a complete I2C driver from scratch that communicates with an SSD1306 OLED display, drawing pixels, text, and simple graphics without using any libraries.

Quick Reference

Attribute Value
Difficulty Level 3 - Advanced
Time Estimate 1-2 weeks
Language C (primary), Rust, MicroPython (for comparison)
Prerequisites Project 3 (bare-metal), Project 4 (peripheral experience), bit manipulation
Key Topics I2C Protocol, GPIO Alternate Functions, SSD1306 Controller, Frame Buffers

1. Learning Objectives

After completing this project, you will:

  • Understand the I2C protocol at the bit and byte level
  • Master GPIO alternate function configuration for I2C pins
  • Implement I2C master mode with proper timing
  • Handle ACK/NACK signaling and error conditions
  • Initialize and control the SSD1306 OLED controller
  • Manage a frame buffer for efficient display updates
  • Implement basic graphics primitives (pixels, lines, text)
  • Understand clock stretching and multi-master arbitration
  • Debug I2C issues with logic analyzers and oscilloscopes

2. Theoretical Foundation

2.1 Core Concepts

The I2C Protocol

I2C (Inter-Integrated Circuit) is a two-wire serial protocol developed by Philips. It uses:

  • SDA: Serial Data line (bidirectional)
  • SCL: Serial Clock line (driven by master)

Both lines require pull-up resistors because devices use open-drain outputs.

I2C Bus Topology:
                        VCC
                         │
                    ┌────┴────┐
                    │  Pull-up │
                    │ Resistors│
                    │ 4.7kΩ ea │
                    └────┬────┘
                         │
         ┌───────────────┼───────────────┐
         │               │               │
    ┌────┴────┐    ┌────┴────┐    ┌────┴────┐
    │  Master │    │  Slave  │    │  Slave  │
    │  (MCU)  │    │ (OLED)  │    │ (Sensor)│
    │         │    │ 0x3C    │    │ 0x68    │
    └────┬────┘    └────┬────┘    └────┬────┘
         │               │               │
         └───────────────┼───────────────┘
                         │
                        GND

Each device has a 7-bit address (0x3C for SSD1306)

I2C Transaction Format

Complete I2C Write Transaction:
┌─────┬──────────────┬─────┬──────────────┬─────┬──────────────┬─────┬─────┐
│START│ Address + W  │ ACK │  Data Byte 1 │ ACK │  Data Byte N │ ACK │STOP │
│     │  (8 bits)    │     │   (8 bits)   │     │   (8 bits)   │     │     │
└─────┴──────────────┴─────┴──────────────┴─────┴──────────────┴─────┴─────┘
   │         │          │
   │         │          └── Slave pulls SDA low to acknowledge
   │         │
   │         └── 7-bit address + R/W bit (0=Write, 1=Read)
   │
   └── SDA goes low while SCL is high

Timing Detail:
       START                     STOP
         │                         │
SCL  ────┐     ┌──┐  ┌──┐  ┌──┐   │   ┌────
         │     │  │  │  │  │  │   │   │
         └─────┘  └──┘  └──┘  └───┴───┘

SDA  ──┐       ┌─────────────────┐
       │ D7    │      ...        │ D0
       └───────┴─────────────────┴────────────
       │                              │
    START                           STOP

SSD1306 OLED Controller

The SSD1306 is a popular OLED driver IC that supports 128x64 or 128x32 pixel displays:

SSD1306 Memory Organization:
┌─────────────────────────────────────────────────────────────────────────────┐
│                           128 columns (0-127)                               │
├─────────────────────────────────────────────────────────────────────────────┤
│ Page 0 │ Each page is 8 pixels tall                                         │
│ (0-7)  │ Each column byte represents 8 vertical pixels                      │
├────────┤                                                                    │
│ Page 1 │                                                                    │
├────────┤                                                                    │
│ Page 2 │                                                                    │
├────────┤   Display: 128x64 = 8 pages x 128 columns = 1024 bytes            │
│ Page 3 │                                                                    │
├────────┤                                                                    │
│ Page 4 │                                                                    │
├────────┤                                                                    │
│ Page 5 │                                                                    │
├────────┤                                                                    │
│ Page 6 │                                                                    │
├────────┤                                                                    │
│ Page 7 │                                                                    │
└─────────────────────────────────────────────────────────────────────────────┘

Single Column Byte (Page N, Column M):
     MSB                    LSB
    ┌───┬───┬───┬───┬───┬───┬───┬───┐
    │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │  ← Bit position
    └───┴───┴───┴───┴───┴───┴───┴───┘
      │   │   │   │   │   │   │   │
      │   │   │   │   │   │   │   └── Row N*8 + 0 (top)
      │   │   │   │   │   │   └────── Row N*8 + 1
      │   │   │   │   │   └────────── Row N*8 + 2
      │   │   │   │   └────────────── Row N*8 + 3
      │   │   │   └────────────────── Row N*8 + 4
      │   │   └────────────────────── Row N*8 + 5
      │   └────────────────────────── Row N*8 + 6
      └────────────────────────────── Row N*8 + 7 (bottom)

I2C Command Protocol for SSD1306

The SSD1306 uses a control byte to distinguish commands from data:

I2C Frame Format:
┌───────┬─────────┬───────┬─────────┬───────┬─────────┐
│ START │ 0x3C<<1 │  ACK  │ Control │  ACK  │ Payload │...
│       │   + W   │       │  Byte   │       │         │
└───────┴─────────┴───────┴─────────┴───────┴─────────┘

Control Byte:
┌───────┬───────┬───────────────────────────────────────┐
│  Co   │  D/C# │              Reserved (0)             │
│ bit 7 │ bit 6 │           bits 5-0                    │
└───────┴───────┴───────────────────────────────────────┘

Co=0: Continuation, only data follows
Co=1: More control bytes follow

D/C#=0: Next byte is command (0x00)
D/C#=1: Next byte is data (0x40)

Examples:
  0x00 = Command mode (single command)
  0x40 = Data mode (display data follows)
  0x80 = Command mode, more control bytes follow

2.2 Why This Matters

I2C is the most common embedded peripheral protocol. Nearly every sensor, display, EEPROM, and I/O expander uses I2C. Building an I2C driver from scratch teaches:

  • Protocol fundamentals: Understanding timing, addressing, and acknowledgment
  • GPIO configuration: Mastering alternate functions and open-drain outputs
  • Error handling: Dealing with NAKs, bus errors, and arbitration
  • Hardware abstraction: Creating clean APIs for peripheral access

Industry usage:

  • Every smartphone has dozens of I2C devices (sensors, touch controllers, etc.)
  • Automotive systems use I2C for dashboard displays and sensor networks
  • Industrial equipment relies on I2C for configuration EEPROMs
  • IoT devices use I2C OLED displays for user interfaces

Plus, you get visible output! Nothing beats seeing pixels light up from your own driver code.

2.3 Historical Context

I2C was developed by Philips Semiconductor (now NXP) in 1982 to simplify communication between ICs on a circuit board. Key innovations:

  • Two-wire design: Minimized pin count and PCB traces
  • Multi-master: Multiple controllers can share the bus
  • Addressing: Up to 128 devices (7-bit) or 1024 devices (10-bit)

The protocol has evolved through several speed grades:

  • Standard mode: 100 kbit/s (1982)
  • Fast mode: 400 kbit/s (1992)
  • Fast mode plus: 1 Mbit/s (2007)
  • High speed: 3.4 Mbit/s (1998)

The SSD1306 controller was designed by Solomon Systech and became the de facto standard for small OLED displays due to its simple interface and low cost.

2.4 Common Misconceptions

Misconception 1: “I2C is always slow”

  • Reality: Fast mode (400 kHz) is sufficient for most displays. The SSD1306 can update its full display at ~60 fps in fast mode.

Misconception 2: “I2C needs external pull-ups”

  • Reality: While recommended for reliable operation, many MCUs have internal pull-ups that work for short buses with few devices.

Misconception 3: “The address 0x3C is 8 bits”

  • Reality: It’s a 7-bit address. When shifted left and combined with R/W bit, it becomes 0x78 (write) or 0x79 (read).

Misconception 4: “ACK means success”

  • Reality: ACK only means the byte was received. It doesn’t guarantee the command was valid or executed correctly.

Misconception 5: “Open-drain means no configuration needed”

  • Reality: GPIO must be explicitly configured as open-drain/alternate function, or SDA will be driven high, causing bus conflicts.

3. Project Specification

3.1 What You Will Build

A complete I2C display system that:

  • Implements I2C master mode from scratch
  • Initializes and controls an SSD1306 OLED display
  • Maintains a frame buffer for efficient updates
  • Provides graphics primitives (pixels, lines, rectangles, text)
  • Works on STM32 or similar Cortex-M boards

3.2 Functional Requirements

  1. I2C driver: Initialize, write single byte, write multiple bytes
  2. Device detection: Scan bus for responding devices
  3. Display initialization: Full SSD1306 init sequence
  4. Frame buffer: 1024-byte buffer for 128x64 display
  5. Pixel operations: Set, clear, toggle individual pixels
  6. Graphics primitives: Lines, rectangles, circles
  7. Text rendering: 8x8 font, string printing
  8. Display update: Transfer frame buffer to display

3.3 Non-Functional Requirements

  1. Speed: Support 400 kHz I2C clock (fast mode)
  2. Reliability: Handle NAK and bus errors gracefully
  3. Efficiency: Use page-based updates when possible
  4. Memory: Frame buffer only uses 1KB RAM
  5. Portability: Clean separation between I2C driver and display code

3.4 Example Usage / Output

Physical result: 128x64 OLED display shows:

┌──────────────────────────────────────┐
│  ARM I2C Demo                        │
│                                      │
│  ╭──────────────────────────╮        │
│  │    CPU: 72MHz            │        │
│  │    Temp: 32C             │        │
│  │    Time: 14:23:45        │        │
│  ╰──────────────────────────╯        │
│                      ___             │
│     *              /ARM \            │
│                    \_____/           │
└──────────────────────────────────────┘

Serial output:
=== I2C OLED Driver Demo ===
Scanning I2C bus...
  Device found at 0x3C (SSD1306 OLED)
I2C initialized at 400kHz
SSD1306 found at address 0x3C
Display initialized (128x64)
Drawing test pattern...
Framebuffer: 1024 bytes (128x64/8)
Frame rate: 62 fps

3.5 Real World Outcome

What success looks like:

  1. Working I2C communication: Device responds to address
  2. Display initialization: Screen turns on, shows content
  3. Graphics output: Pixels, text, and shapes render correctly
  4. Efficient updates: Full screen updates at reasonable frame rate
  5. Deep understanding: You can explain I2C timing diagrams

4. Solution Architecture

4.1 High-Level Design

┌─────────────────────────────────────────────────────────────────┐
│                    I2C OLED Display System                       │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                   Application Layer                      │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │    │
│  │  │   Shapes    │  │    Text     │  │   Sprites   │      │    │
│  │  │  draw_rect  │  │ print_str   │  │ draw_bitmap │      │    │
│  │  │ draw_circle │  │ print_char  │  │             │      │    │
│  │  │  draw_line  │  │             │  │             │      │    │
│  │  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘      │    │
│  └─────────│────────────────│────────────────│─────────────┘    │
│            │                │                │                   │
│            ▼                ▼                ▼                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                  Frame Buffer Layer                      │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │         uint8_t framebuffer[1024]                 │  │    │
│  │  │                                                   │  │    │
│  │  │  set_pixel(x, y)    clear_pixel(x, y)            │  │    │
│  │  │  get_pixel(x, y)    clear_buffer()               │  │    │
│  │  └───────────────────────────────────────────────────┘  │    │
│  └────────────────────────────┬────────────────────────────┘    │
│                               │                                  │
│                               ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                  SSD1306 Driver Layer                    │    │
│  │  ┌───────────────────┐  ┌───────────────────┐           │    │
│  │  │    ssd1306_init   │  │  ssd1306_display  │           │    │
│  │  │  send_command     │  │  send_data        │           │    │
│  │  │  set_contrast     │  │  set_addressing   │           │    │
│  │  └─────────┬─────────┘  └─────────┬─────────┘           │    │
│  └────────────│──────────────────────│─────────────────────┘    │
│               │                      │                           │
│               ▼                      ▼                           │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                    I2C Driver Layer                      │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │  i2c_init()      i2c_start()      i2c_stop()      │  │    │
│  │  │  i2c_write()     i2c_write_bytes()                │  │    │
│  │  │  i2c_scan()                                       │  │    │
│  │  └───────────────────────────────────────────────────┘  │    │
│  └────────────────────────────┬────────────────────────────┘    │
│                               │                                  │
│                               ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                   Hardware Layer                         │    │
│  │  ┌───────────────────────────────────────────────────┐  │    │
│  │  │  I2C1 Peripheral        GPIO PB6 (SCL), PB7 (SDA) │  │    │
│  │  │  Clock Configuration    Pull-ups (internal/ext)   │  │    │
│  │  └───────────────────────────────────────────────────┘  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

4.2 Key Components

Component Purpose Key Functions
I2C Driver Low-level I2C communication i2c_init(), i2c_write(), i2c_scan()
SSD1306 Driver Display-specific commands ssd1306_init(), ssd1306_display()
Frame Buffer In-memory display state set_pixel(), clear_buffer()
Graphics Library Drawing primitives draw_line(), draw_rect(), draw_circle()
Text Renderer Character and string output print_char(), print_str()
Font Data 8x8 bitmap font font8x8[] array

4.3 Data Structures

// I2C configuration
typedef struct {
    I2C_TypeDef *i2c;      // I2C peripheral (I2C1, I2C2, etc.)
    uint32_t clock_speed;   // 100000 or 400000 Hz
    uint8_t own_address;    // Master address (if needed)
} i2c_config_t;

// Display configuration
typedef struct {
    uint8_t width;          // 128 pixels
    uint8_t height;         // 64 pixels
    uint8_t i2c_address;    // 0x3C or 0x3D
    uint8_t *framebuffer;   // Pointer to 1024-byte buffer
} ssd1306_t;

// Graphics context
typedef struct {
    ssd1306_t *display;
    uint8_t fg_color;       // 1 = white, 0 = black
    uint8_t bg_color;
    uint8_t cursor_x;
    uint8_t cursor_y;
    const uint8_t *font;    // Pointer to font data
} gfx_context_t;

// Framebuffer (128x64 / 8 bits per vertical byte = 1024 bytes)
uint8_t framebuffer[128 * 64 / 8];

4.4 Algorithm Overview

FUNCTION initialize_display():
    // Phase 1: Enable clocks
    enable_gpio_clock()
    enable_i2c_clock()

    // Phase 2: Configure GPIO for I2C
    configure_gpio_alternate_function(SDA_PIN, AF_I2C)
    configure_gpio_alternate_function(SCL_PIN, AF_I2C)
    set_output_type(SDA_PIN, OPEN_DRAIN)
    set_output_type(SCL_PIN, OPEN_DRAIN)

    // Phase 3: Configure I2C peripheral
    disable_i2c()
    set_clock_speed(400000)
    enable_i2c()

    // Phase 4: Initialize SSD1306
    send_init_commands()

    // Phase 5: Clear display
    clear_framebuffer()
    send_framebuffer_to_display()


FUNCTION draw_pixel(x, y, color):
    IF x >= 128 OR y >= 64:
        RETURN  // Out of bounds

    page = y / 8
    bit = y % 8
    offset = page * 128 + x

    IF color:
        framebuffer[offset] |= (1 << bit)
    ELSE:
        framebuffer[offset] &= ~(1 << bit)


FUNCTION send_framebuffer_to_display():
    // Set addressing mode and column/page addresses
    send_command(0x20, 0x00)  // Horizontal addressing mode
    send_command(0x21, 0, 127)  // Column range
    send_command(0x22, 0, 7)    // Page range

    // Send all 1024 bytes
    i2c_start()
    i2c_write(SSD1306_ADDRESS << 1)
    i2c_write(0x40)  // Data mode
    FOR i = 0 TO 1023:
        i2c_write(framebuffer[i])
    i2c_stop()

5. Implementation Guide

5.1 Development Environment Setup

# Verify toolchain
$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (GNU Arm Embedded Toolchain) 12.2.1

# Create project directory
$ mkdir -p ~/projects/i2c-oled
$ cd ~/projects/i2c-oled

# Create initial file structure
$ touch main.c i2c.c i2c.h ssd1306.c ssd1306.h gfx.c gfx.h font8x8.h
$ touch startup.s linker.ld Makefile

# Hardware connections:
#   STM32 PB6 (SCL) ──── SSD1306 SCL
#   STM32 PB7 (SDA) ──── SSD1306 SDA
#   STM32 3.3V ───────── SSD1306 VCC
#   STM32 GND ────────── SSD1306 GND
#   (4.7kΩ pull-ups on SCL and SDA to VCC recommended)

5.2 Project Structure

i2c-oled/
├── main.c              # Demo application
├── i2c.c               # I2C driver implementation
├── i2c.h               # I2C driver interface
├── ssd1306.c           # SSD1306 display driver
├── ssd1306.h           # Display driver interface
├── gfx.c               # Graphics primitives
├── gfx.h               # Graphics interface
├── font8x8.h           # 8x8 bitmap font data
├── stm32f4xx.h         # Register definitions
├── startup.s           # Vector table and reset handler
├── linker.ld           # Linker script
├── Makefile
└── README.md

5.3 The Core Question You’re Answering

“How does a two-wire protocol enable communication with dozens of different devices, and how do displays organize and receive pixel data?”

Before coding, understand that I2C’s elegance lies in its addressing scheme and open-drain signaling. Every device monitors the bus, but only responds when its address is called. The SSD1306 adds another layer: a command/data protocol on top of I2C bytes.

5.4 Concepts You Must Understand First

Stop and research these before coding:

  1. Open-Drain Outputs
    • Why can’t you drive SDA high directly?
    • How do pull-up resistors enable the bus?
    • Book Reference: “Making Embedded Systems” Chapter 9 - White
  2. I2C Timing
    • What are setup and hold times?
    • How is clock speed determined?
    • Book Reference: I2C Specification (NXP)
  3. GPIO Alternate Functions
    • What does “alternate function” mean?
    • How do you find the correct AF number?
    • Book Reference: STM32 Reference Manual, GPIO chapter
  4. SSD1306 Command Set
    • What’s the difference between commands and data?
    • How does addressing mode affect data writes?
    • Book Reference: SSD1306 Datasheet

5.5 Questions to Guide Your Design

Before implementing, think through these:

  1. Bus Configuration
    • What pull-up resistance is optimal for your bus length and speed?
    • Should you use internal or external pull-ups?
  2. Error Handling
    • What if the device doesn’t ACK?
    • How do you recover from a stuck bus?
  3. Performance
    • Should you update the whole display or just changed regions?
    • Can you use DMA for large transfers?
  4. Memory
    • Where should the frame buffer live?
    • What if you don’t have 1KB for a full buffer?

5.6 Thinking Exercise

Trace an I2C Write

Before coding, trace what happens when you call i2c_write_byte(0x3C, 0xAF):

1. Generate START condition
   - SCL high, SDA transitions high→low
   - All devices see START, prepare to listen

2. Send address byte (0x78 = 0x3C << 1 | 0)
   - Clock out 8 bits: 0111 1000
   - Each bit: set SDA, pulse SCL
   - Bit 0 = 0 indicates WRITE operation

3. Wait for ACK
   - Release SDA (goes high via pull-up)
   - Clock SCL
   - SSD1306 pulls SDA low = ACK
   - If SDA stays high = NACK (device not present)

4. Send data byte (0xAF)
   - Clock out 8 bits: 1010 1111
   - This is "Display ON" command for SSD1306

5. Wait for ACK
   - Same as step 3

6. Generate STOP condition
   - SDA transitions low→high while SCL high
   - Bus is released

Questions while tracing:
- What voltage levels represent '0' and '1'?
- Who drives each wire at each step?
- What happens if you skip the START?

5.7 Hints in Layers

Hint 1: GPIO Configuration

Configure GPIO pins for I2C before enabling the I2C peripheral:

// Enable GPIO clock
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

// Configure PB6 and PB7 for I2C1
// Mode: Alternate Function (10)
GPIOB->MODER &= ~((3 << (6*2)) | (3 << (7*2)));
GPIOB->MODER |= (2 << (6*2)) | (2 << (7*2));

// Output type: Open Drain (1)
GPIOB->OTYPER |= (1 << 6) | (1 << 7);

// Speed: High (11)
GPIOB->OSPEEDR |= (3 << (6*2)) | (3 << (7*2));

// Pull-up: Enable (01)
GPIOB->PUPDR &= ~((3 << (6*2)) | (3 << (7*2)));
GPIOB->PUPDR |= (1 << (6*2)) | (1 << (7*2));

// Alternate function: AF4 for I2C1
GPIOB->AFR[0] &= ~((0xF << (6*4)) | (0xF << (7*4)));
GPIOB->AFR[0] |= (4 << (6*4)) | (4 << (7*4));

Hint 2: I2C Peripheral Setup

// Enable I2C1 clock
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

// Reset and configure I2C
I2C1->CR1 = 0;                    // Disable I2C
I2C1->CR2 = 36;                   // APB1 clock = 36MHz
I2C1->CCR = 90;                   // CCR for 400kHz: 36MHz/(2*400kHz)
I2C1->TRISE = 11;                 // Rise time: 36MHz * 300ns + 1
I2C1->CR1 = I2C_CR1_PE;           // Enable I2C

Hint 3: I2C Write Function

bool i2c_write(uint8_t addr, uint8_t *data, uint8_t len) {
    // Generate START
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB));

    // Send address with write bit
    I2C1->DR = addr << 1;
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR2;  // Clear ADDR flag by reading SR2

    // Send data bytes
    for (int i = 0; i < len; i++) {
        while (!(I2C1->SR1 & I2C_SR1_TXE));
        I2C1->DR = data[i];
    }

    // Wait for last byte to finish
    while (!(I2C1->SR1 & I2C_SR1_BTF));

    // Generate STOP
    I2C1->CR1 |= I2C_CR1_STOP;

    return true;
}

Hint 4: SSD1306 Initialization

static const uint8_t init_cmds[] = {
    0xAE,       // Display off
    0xD5, 0x80, // Set display clock divide
    0xA8, 0x3F, // Set multiplex ratio (64-1)
    0xD3, 0x00, // Set display offset
    0x40,       // Set start line to 0
    0x8D, 0x14, // Enable charge pump
    0x20, 0x00, // Horizontal addressing mode
    0xA1,       // Segment remap (column 127 = SEG0)
    0xC8,       // COM scan direction (remapped)
    0xDA, 0x12, // Set COM pins configuration
    0x81, 0xCF, // Set contrast
    0xD9, 0xF1, // Set pre-charge period
    0xDB, 0x40, // Set VCOMH deselect level
    0xA4,       // Display from RAM
    0xA6,       // Normal display (not inverted)
    0xAF        // Display on
};

void ssd1306_init(void) {
    for (int i = 0; i < sizeof(init_cmds); i++) {
        ssd1306_command(init_cmds[i]);
    }
}

void ssd1306_command(uint8_t cmd) {
    uint8_t data[2] = {0x00, cmd};  // 0x00 = command mode
    i2c_write(SSD1306_ADDR, data, 2);
}

5.8 The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain how I2C addressing works”
    • 7-bit address (128 possible devices)
    • Address sent after START, with R/W bit as LSB
    • Device with matching address responds with ACK
    • Reserved addresses: 0x00 (general call), 0x7F (reserved)
  2. “Why does I2C use open-drain outputs?”
    • Allows multiple devices to drive the same line
    • Prevents short circuits (no driver conflict)
    • Enables wired-AND logic (any device can pull low)
    • Requires pull-up resistors to achieve logic high
  3. “What is clock stretching?”
    • Slave holds SCL low to slow down master
    • Used when slave needs more time to process data
    • Master must detect and wait for SCL to go high
    • Can cause timing issues with strict masters
  4. “How would you debug an I2C device that’s not responding?”
    • Verify address with i2c_scan()
    • Check pull-up resistors (measure voltage levels)
    • Use oscilloscope/logic analyzer to view signals
    • Check GPIO alternate function configuration
    • Verify clock enable for GPIO and I2C peripherals
  5. “Why does the SSD1306 organize memory in pages?”
    • Historical: early OLEDs were segmented displays
    • Efficient: can update partial screen
    • Trade-off: complicates pixel addressing
    • 8 vertical pixels per byte matches segment drivers

5.9 Books That Will Help

Topic Book Chapter
I2C protocol “Making Embedded Systems” by White Ch. 9
GPIO configuration STM32 Reference Manual GPIO chapter
Display controllers SSD1306 Datasheet All
Graphics primitives “Computer Graphics from Scratch” by Gambetta Ch. 1
Protocol analysis “Debugging with GDB and strace” I2C section

5.10 Implementation Phases

Phase 1: I2C Bus Scan (2-3 hours)

  • Configure GPIO and I2C peripheral
  • Implement i2c_start(), i2c_stop(), i2c_write_byte()
  • Write bus scanner to find devices
  • Verify SSD1306 responds at 0x3C

Phase 2: Display Initialization (2-3 hours)

  • Implement command sending function
  • Send SSD1306 initialization sequence
  • Display should turn on (may show noise)

Phase 3: Frame Buffer (3-4 hours)

  • Allocate 1024-byte frame buffer
  • Implement set_pixel(), clear_buffer()
  • Implement frame buffer to display transfer
  • Clear display should show blank screen

Phase 4: Graphics Primitives (4-6 hours)

  • Implement draw_line() using Bresenham’s algorithm
  • Implement draw_rect(), fill_rect()
  • Implement draw_circle() (optional)
  • Test with geometric patterns

Phase 5: Text Rendering (3-4 hours)

  • Create or import 8x8 font data
  • Implement print_char()
  • Implement print_str()
  • Display “Hello ARM!” on screen

Phase 6: Demo and Polish (2-3 hours)

  • Create impressive demo (animation, info display)
  • Optimize frame rate
  • Add error handling
  • Document API

5.11 Key Implementation Decisions

Decision Trade-offs
Full vs partial update Full: simpler. Partial: faster for small changes
Internal vs external pull-ups Internal: fewer components. External: more reliable
Blocking vs DMA Blocking: simpler. DMA: CPU free during transfer
Software vs hardware I2C HW: faster, handles timing. SW: more portable
Font storage Flash: saves RAM. RAM: faster access

6. Testing Strategy

6.1 Unit Tests

Test Description Expected Result
GPIO config Check MODER, OTYPER, AFR values Correct bit patterns
I2C clock Measure SCL frequency 400 kHz +/- 10%
Bus scan Scan for devices 0x3C detected
START/STOP Check signal timing Correct transitions
ACK detection Send to invalid address Returns false/NACK

6.2 Integration Tests

Test Command Expected Result
Display init Call ssd1306_init() Display turns on
Clear display clear_buffer(); update() Blank screen
Single pixel set_pixel(64, 32, 1) Center pixel lit
Horizontal line draw_line(0,0,127,0) Top edge lit
Vertical line draw_line(0,0,0,63) Left edge lit
Text print_str(“ARM”) “ARM” visible
Full frame Fill pattern, update Pattern displayed

6.3 Verification Commands

# Use logic analyzer to capture I2C traffic
$ sigrok-cli -d fx2lafw -c samplerate=2M -P i2c:scl=D0:sda=D1 -A i2c

# Expected output:
# i2c-1: Address write: 3C
# i2c-1: ACK
# i2c-1: Data write: 00
# i2c-1: ACK
# i2c-1: Data write: AE
# ...

# Measure frame rate
# Add timer code to measure update frequency
# Expected: 30-60 fps depending on optimization

7. Common Pitfalls & Debugging

Problem 1: “No ACK received (device not found)”

  • Why: Wrong address, no pull-ups, GPIO not configured
  • Fix: Check address (0x3C or 0x3D), add pull-ups, verify GPIO config
  • Debug: Measure SDA/SCL with oscilloscope, should see toggling

Problem 2: “Display shows garbage”

  • Why: Wrong initialization sequence or addressing mode
  • Fix: Verify init commands match datasheet, check addressing mode
  • Debug: Compare init sequence byte-by-byte with working example

Problem 3: “I2C hangs (no response)”

  • Why: Missed ACK check, stuck bus, clock stretching timeout
  • Fix: Add timeout to all waits, implement bus recovery
  • Debug: Toggle GPIO manually to unstick bus

Problem 4: “Pixels appear in wrong location”

  • Why: Column/row addressing confusion, byte order wrong
  • Fix: Remember: each byte is 8 vertical pixels, LSB is top
  • Debug: Set single pixels at known coordinates, trace buffer index

Problem 5: “Text is garbled or upside down”

  • Why: Font bit order doesn’t match display organization
  • Fix: Adjust font rendering to match SSD1306’s column/page layout
  • Debug: Print single character, compare visual with font data

Problem 6: “Display flickers or updates slowly”

  • Why: Updating entire display for small changes, I2C too slow
  • Fix: Use partial updates, increase I2C speed, consider DMA
  • Debug: Measure actual frame rate, profile transfer time

8. Extensions & Challenges

8.1 Easy Extensions

Extension Description Learning
Contrast control Runtime contrast adjustment Command parameters
Display invert Toggle inverse video mode Simple command
Scrolling Hardware scroll commands SSD1306 scroll features
Multiple fonts Add 16x16 or proportional fonts Font rendering

8.2 Advanced Challenges

Challenge Description Learning
DMA transfer Use DMA for frame buffer transfer DMA peripheral
Double buffering Render to back buffer, swap Animation smoothness
Sprites XOR-based sprite rendering Game development
Touch input Add I2C touch controller Multiple I2C devices
Animation engine Frame-based animation system Graphics programming
Display rotation Support 0/90/180/270 rotation Coordinate transforms

8.3 Research Topics

  • How do commercial graphics libraries (LVGL, u8g2) optimize for I2C displays?
  • What is the maximum practical frame rate for I2C OLED displays?
  • How do high-performance displays use parallel interfaces instead of I2C?
  • What are the power consumption implications of different update strategies?

9. Real-World Connections

9.1 Production Systems Using This

System How It Uses I2C Displays Notable Feature
Arduino ecosystem u8g2 library Universal display support
3D printers Marlin firmware Status displays
Smart watches Low-power OLED Sleep modes
IoT devices Status indicators Minimal hardware
Lab equipment Data displays Real-time updates

9.2 How the Pros Do It

u8g2 Library:

  • Universal HAL for many MCU families
  • Multiple buffer strategies (full, page, tile)
  • Hardware and software I2C support
  • Extensive font library

LVGL:

  • Professional embedded GUI library
  • Display driver abstraction
  • DMA acceleration support
  • Complex widget system

Production IoT:

  • Power-gated displays for battery life
  • Partial update regions for efficiency
  • Error recovery for field reliability

10. Self-Assessment Checklist

Before considering this project complete, verify:

  • I can explain the I2C protocol including START, STOP, ACK, and addressing
  • I understand why open-drain outputs and pull-up resistors are required
  • My I2C driver correctly handles device addressing and ACK checking
  • I can scan the bus and identify connected devices
  • I understand the SSD1306’s memory organization (pages and columns)
  • My display initialization sequence correctly turns on the display
  • Frame buffer updates correctly transfer to the display
  • I can draw pixels at any coordinate
  • I implemented at least one drawing primitive (line, rectangle)
  • Text rendering works correctly with the 8x8 font
  • I can achieve reasonable frame rates (30+ fps for full updates)
  • I can debug I2C issues using oscilloscope or logic analyzer
  • I can answer all the interview questions listed above

Next Steps

After completing this project, you’ll be well-prepared for:

  • Project 11: SPI SD Card Driver - Another essential peripheral protocol
  • Project 13: DMA Audio Player - Apply DMA concepts to any peripheral
  • Game Console Capstone - Use display for game graphics

The I2C skills you’ve developed are immediately applicable to hundreds of different sensors and peripherals. You now understand the most common embedded protocol!