A 0.96-inch SSD1306 OLED is the standard little status screen for ESP32 projects. On the 38-pin DevKitC it uses the same default I2C pins as the 30-pin DevKit V1 — GPIO21 (SDA) and GPIO22 (SCL) — but they are printed as plain 21 and 22, and the header around them is laid out differently.
The sketch is more than a “hello world”: it scans the bus, finds the OLED at 0x3C or 0x3D on its own, and shows what is actually on your board — chip model and silicon version, cores, clock, real flash-chip size and MAC address. With several unbranded 38-pin clones in a drawer, that tells you which is which.
Wiring
| ESP32 DevKitC (38-pin) | SSD1306 OLED 0.96″ (128×64, I2C) | Wire | Notes |
|---|---|---|---|
| GND | GND | black | The GND at the top of the right header (J3 pin 1), right next to 22 and 21. |
| 3V3 | VCC | red | 3V3 is only on the left header (top). Not 5V — the OLED’s pull-ups would put 5 V on SDA/SCL. |
| 22 | SCL | yellow | Default I2C clock (GPIO22). |
| 21 | SDA | blue | Default I2C data (GPIO21). TX and RX sit between 22 and 21 — don’t shift a wire by one. |
3.3–5 V (on-board regulator) part, 3.3 V board. Most modules have a regulator and run from 3.3 V or 5 V. Their I2C pull-ups go to VCC, so on ESP32 and Pico power the OLED from 3.3 V to keep SDA/SCL at a safe level.
Board quirks
- On the 38-pin board the right header reads
GND, 23, 22, TX, RX, 21, GNDfrom the top. SDA and SCL are three holes apart with the USB-serial pins TX and RX in between; a wire one hole off lands on TX and garbles uploads. - The board has three GND pins: one on the left header (between 12 and 13) and two on the right (top, and below 21). Any of them works; the top-right one keeps the OLED wires short.
- There is no 3V3 on the right header. The VCC wire has to cross from the top-left pin — route it around the antenna end, not over the module’s metal can.
- Check the order printed on your OLED. Some clones are
VCC, GND, SCL, SDAinstead ofGND, VCC…; reversed power kills the display. - The board-ID screen is useful with clones: the silicon version (v3.x is the newer chip found in WROOM-32E modules, v1.x the older one) and the size of the flash chip itself, read from the chip rather than from the Arduino IDE’s “Flash Size” setting. The exact model string differs between chip packages.
Code
// SSD1306 0.96" I2C OLED as a "which board is this?" screen on an ESP32 DevKitC (38-pin).
// GND → GND (right header, top), VCC → 3V3 (left header, top), SCL → 22, SDA → 21.
// Scans the I2C bus, then shows the chip model, revision, real flash-chip size and MAC —
// handy when a drawer holds several look-alike 38-pin clones.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <esp_flash.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
uint8_t oledAddr = 0;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA, SCL — printed 21 and 22 on the right header
Serial.print("I2C devices:");
for (uint8_t a = 1; a < 127; a++) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) {
Serial.printf(" 0x%02X", a);
if (a == 0x3C || a == 0x3D) oledAddr = a;
}
}
Serial.println();
if (!oledAddr || !display.begin(SSD1306_SWITCHCAPVCC, oledAddr)) {
Serial.println("No SSD1306 at 0x3C/0x3D — check GND/VCC order on your module and SDA=21, SCL=22");
while (true) delay(1000);
}
uint32_t flashBytes = 0;
esp_flash_get_physical_size(esp_flash_default_chip, &flashBytes); // the chip itself, not the build setting
uint16_t rev = ESP.getChipRevision(); // e.g. 301 = silicon v3.1
uint8_t mac[6];
uint64_t efuse = ESP.getEfuseMac();
memcpy(mac, &efuse, 6);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("%s\n", ESP.getChipModel());
display.printf("Silicon v%d.%d\n", rev / 100, rev % 100);
display.printf("%d cores, %lu MHz\n", ESP.getChipCores(), (unsigned long)ESP.getCpuFreqMHz());
display.printf("Flash chip %lu MB\n", (unsigned long)(flashBytes / (1024 * 1024)));
display.printf("MAC %02X%02X%02X%02X%02X%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
display.printf("OLED at 0x%02X\n", oledAddr);
display.display();
Serial.printf("%s, silicon v%d.%d, %d cores, %lu MHz, flash chip %lu MB, MAC %02X:%02X:%02X:%02X:%02X:%02X\n",
ESP.getChipModel(), rev / 100, rev % 100, ESP.getChipCores(), (unsigned long)ESP.getCpuFreqMHz(),
(unsigned long)(flashBytes / (1024 * 1024)), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
void loop() {
// Bottom line: uptime, redrawn once a second.
static uint32_t last = 0;
if (millis() - last < 1000) return;
last = millis();
display.fillRect(0, 48, 128, 16, SSD1306_BLACK);
display.setCursor(0, 52);
display.printf("Up %lus", (unsigned long)(millis() / 1000));
display.display();
}// SSD1306 0.96" I2C OLED as a "which board is this?" screen on an ESP32 DevKitC (38-pin).
// GND → GND (right header, top), VCC → 3V3 (left header, top), SCL → 22, SDA → 21.
// Scans the I2C bus, then shows the chip model, revision, real flash-chip size and MAC —
// handy when a drawer holds several look-alike 38-pin clones.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <esp_flash.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
uint8_t oledAddr = 0;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA, SCL — printed 21 and 22 on the right header
Serial.print("I2C devices:");
for (uint8_t a = 1; a < 127; a++) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) {
Serial.printf(" 0x%02X", a);
if (a == 0x3C || a == 0x3D) oledAddr = a;
}
}
Serial.println();
if (!oledAddr || !display.begin(SSD1306_SWITCHCAPVCC, oledAddr)) {
Serial.println("No SSD1306 at 0x3C/0x3D — check GND/VCC order on your module and SDA=21, SCL=22");
while (true) delay(1000);
}
uint32_t flashBytes = 0;
esp_flash_get_physical_size(esp_flash_default_chip, &flashBytes); // the chip itself, not the build setting
uint16_t rev = ESP.getChipRevision(); // e.g. 301 = silicon v3.1
uint8_t mac[6];
uint64_t efuse = ESP.getEfuseMac();
memcpy(mac, &efuse, 6);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("%s\n", ESP.getChipModel());
display.printf("Silicon v%d.%d\n", rev / 100, rev % 100);
display.printf("%d cores, %lu MHz\n", ESP.getChipCores(), (unsigned long)ESP.getCpuFreqMHz());
display.printf("Flash chip %lu MB\n", (unsigned long)(flashBytes / (1024 * 1024)));
display.printf("MAC %02X%02X%02X%02X%02X%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
display.printf("OLED at 0x%02X\n", oledAddr);
display.display();
Serial.printf("%s, silicon v%d.%d, %d cores, %lu MHz, flash chip %lu MB, MAC %02X:%02X:%02X:%02X:%02X:%02X\n",
ESP.getChipModel(), rev / 100, rev % 100, ESP.getChipCores(), (unsigned long)ESP.getCpuFreqMHz(),
(unsigned long)(flashBytes / (1024 * 1024)), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
void loop() {
// Bottom line: uptime, redrawn once a second.
static uint32_t last = 0;
if (millis() - last < 1000) return;
last = millis();
display.fillRect(0, 48, 128, 16, SSD1306_BLACK);
display.setCursor(0, 52);
display.printf("Up %lus", (unsigned long)(millis() / 1000));
display.display();
}Library: Adafruit SSD1306 (Adafruit (also install Adafruit GFX Library)) — compiled with Adafruit SSD1306 2.5.17.
Expected serial output
I2C devices: 0x3C
ESP32-D0WDQ6-V3, silicon v3.0, 2 cores, 240 MHz, flash chip 4 MB, MAC 24:6F:28:A1:B2:C3I2C devices: 0x3C ESP32-D0WDQ6-V3, silicon v3.0, 2 cores, 240 MHz, flash chip 4 MB, MAC 24:6F:28:A1:B2:C3
Common errors
“No SSD1306 at 0x3C/0x3D” and no I2C devices listed
Nothing answers on 21/22. Check SDA is on the pin printed 21 and SCL on 22, VCC on 3V3, and that power isn’t reversed. Pins 21/22 on clones may be printed G21/G22 or IO21/IO22.
Upload fails after wiring the OLED
A wire is on TX or RX (between 22 and 21). Those are the USB-serial lines; move it one hole over.
Display shows only the top half
It is a 128×32 module. Change the constructor to Adafruit_SSD1306 display(128, 32, &Wire, -1).