---
# System prepended metadata

title: How to Drive an I²C OLED from an FPGA?
tags: [FPGA]

---

Driving an I²C OLED from an [FPGA](https://www.ampheo.com/c/fpgas-field-programmable-gate-array) is basically two parts:

1. build a reliable I²C master (START/STOP/ACK/byte send) with open-drain SDA/SCL,
2. implement an [OLED](https://www.onzuu.com/category/lcd-oled-character-and-numeric) controller state machine that sends the init commands + framebuffer data (most common OLEDs are SSD1306/SH1106).

Below is a practical, “works on real boards” approach.

![maxresdefault (91)](https://hackmd.io/_uploads/rJBkuqmHWg.jpg)

**1) Hardware essentials (don’t skip these)**
**Bus wiring**

* SCL, SDA need pull-up [resistors](https://www.onzuu.com/category/resistors) to the OLED’s I/O voltage (often 3.3V).
  * Typical: 4.7 kΩ (start here), 2.2–10 kΩ depending on bus speed/capacitance.
* FPGA pins must behave like open-drain:
  * Drive 0 strongly
  * For 1, release (Hi-Z) and let pull-ups bring the line high

**Voltage**

* Ensure FPGA I/O bank voltage matches OLED module I/O (3.3V common).
* Many small OLED modules accept 3.3V logic even when powered from 5V (but verify).

**2) I²C basics you must implement in FPGA**

I²C is byte-oriented with:

* START: SDA goes low while SCL high
* STOP: SDA goes high while SCL high
* Write byte: 8 bits MSB first, then ACK bit from slave (SDA pulled low = ACK)

For OLED modules:

* 7-bit slave address is commonly 0x3C or 0x3D
* Write address byte = (addr<<1) | 0 → 0x78 or 0x7A for 0x3C/0x3D

Pick a bus speed

* 100 kHz is easiest and very tolerant.
* 400 kHz is fine on short traces.

You’ll generate SCL by dividing the FPGA clock.

**3) OLED (SSD1306-style) I²C packet format**

After the address byte and ACK, SSD1306 uses a control byte:

* 0x00 = following bytes are commands
* 0x40 = following bytes are data (display RAM)

So you typically send:

* Init: [START][addrW][0x00][cmd1][cmd2]...[STOP]
* Frame update: [START][addrW][0x40][data…many bytes][STOP]

For 128×64 monochrome OLED:

* framebuffer is 128×64/8 = 1024 bytes
* data is often sent in 8 pages × 128 bytes/page

**4) Initialization sequence (typical SSD1306 128×64)**

Different modules can vary (SSD1306 vs SH1106), but this is a common SSD1306 init in page addressing mode (safe starter):

Commands (sent after control byte 0x00):

* AE display OFF
* D5 80 clock divide
* A8 3F multiplex 1/64
* D3 00 display offset
* 40 start line = 0
* 8D 14 charge pump ON (common for I²C modules)
* 20 02 memory mode = page addressing
* A1 segment remap (mirror X) (optional)
* C8 COM scan direction (mirror Y) (optional)
* DA 12 COM pins config (common for 128×64)
* 81 CF contrast (tune)
* D9 F1 pre-charge
* DB 40 VCOMH deselect
* A4 display follows RAM
* A6 normal (not inverted)
* AF display ON

If your image is upside down/mirrored, flip A1/C8.

**5) How to update the display (page write pattern)**

For each page 0..7:

1. set page + column:

* B0 | page (page select)
* 00 (lower column = 0)
* 10 (higher column = 0)

2. send 128 bytes of data with control byte 0x40

**6) Verilog skeleton: open-drain I²C lines**
**Open-drain pin driving (generic)**
```
// drive_low = 1 -> drive '0'
// drive_low = 0 -> release (Hi-Z) so pull-up makes it '1'
assign sda = sda_drive_low ? 1'b0 : 1'bz;
assign scl = scl_drive_low ? 1'b0 : 1'bz;

// Read the actual line level:
wire sda_in = sda;
wire scl_in = scl;
```


On [Xilinx](https://www.ampheo.com/manufacturer/amd) you can also use IOBUF if you prefer, but the above works if the pin supports tri-state.

**7) Verilog idea: I²C byte writer (state machine concept)**

You want a module like:

* Inputs: start, stop, write_byte, byte_in
* Outputs: busy, done, ack_error

Internally it:

* Generates SCL high/low phases
* Puts each data bit on SDA while SCL low, then raises SCL to sample
* Releases SDA on the 9th clock to read ACK

Key rule: change SDA only when SCL is low (except START/STOP).

**8) OLED controller architecture (what to build)**
**Minimal working blocks**

1. i2c_master (byte-level)
2. oled_ctrl (high-level sequences)

oled_ctrl does:

* power-up delay (a few ms)
* send init command list
* periodically push framebuffer (or only when changed)

**Framebuffer options**

* BRAM framebuffer: 1024 bytes for 128×64 (easy)
* Generate on the fly: if you only need text/simple graphics

**9) Debug checklist (saves hours)**

* Confirm pull-ups exist and match voltage.
* Check SCL frequency with a scope (start with 100 kHz).
* Verify you see:
  * START, address byte, ACK low
* If no ACK:
  * wrong address (0x3C vs 0x3D)
  * OLED not powered / wrong voltage
  * SDA/SCL swapped
  * [FPGA](https://www.ampheoelec.de/c/fpgas-field-programmable-gate-array) pins not actually tri-stating for ‘1’