OLED Pong
Classic Pong on a tiny OLED with two knobs as paddles. Play a friend, or set one side to the computer.
Wiring
| From | To board | Note |
|---|---|---|
| OLED VCC | 3V3 | |
| OLED GND | GND | |
| OLED SDA | GPIO 21 | |
| OLED SCL | GPIO 22 | |
| Left knob, middle pin | GPIO 34 | Outer pins of each knob to 3V3 and GND, never 5V |
| Right knob, middle pin | GPIO 35 | Skip it and set TWO_PLAYERS to false to play the computer |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32 Dev Module
// Pong on a 128 x 64 SSD1306 with two potentiometers as paddles.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int KNOB_LEFT = 34; // ADC1 pins
const int KNOB_RIGHT = 35;
const bool TWO_PLAYERS = true; // false: the right paddle plays itself
const int WIN_SCORE = 7;
const int W = 128, H = 64, PADDLE_H = 14, PADDLE_W = 3;
Adafruit_SSD1306 display(W, H, &Wire, -1);
float ballX, ballY, velX, velY;
float leftY = 25, rightY = 25;
int scoreLeft = 0, scoreRight = 0;
void serve(int direction) {
ballX = W / 2;
ballY = H / 2;
velX = 1.6f * direction;
velY = random(-10, 11) / 10.0f;
}
float knobToY(int pin, float current) {
float target = analogRead(pin) * (H - PADDLE_H) / 4095.0f;
return current + (target - current) * 0.5f; // a little smoothing
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 not found at 0x3C");
for (;;) delay(1000);
}
analogReadResolution(12);
randomSeed(esp_random());
serve(1);
Serial.printf("Pong ready. Left paddle: GPIO %d, right paddle: %s\n", KNOB_LEFT, TWO_PLAYERS ? "knob" : "computer");
}
void loop() {
leftY = knobToY(KNOB_LEFT, leftY);
if (TWO_PLAYERS) {
rightY = knobToY(KNOB_RIGHT, rightY);
} else {
float aim = ballY - PADDLE_H / 2;
rightY += constrain(aim - rightY, -1.3f, 1.3f); // beatable on purpose
rightY = constrain(rightY, 0.0f, (float)(H - PADDLE_H));
}
ballX += velX;
ballY += velY;
if (ballY <= 0 || ballY >= H - 2) velY = -velY;
// Paddles: the bounce angle depends on where the ball hits.
if (velX < 0 && ballX <= 4 && ballY + 2 >= leftY && ballY <= leftY + PADDLE_H) {
velX = -velX * 1.05f;
velY = (ballY - (leftY + PADDLE_H / 2)) / 4;
}
if (velX > 0 && ballX >= W - 6 && ballY + 2 >= rightY && ballY <= rightY + PADDLE_H) {
velX = -velX * 1.05f;
velY = (ballY - (rightY + PADDLE_H / 2)) / 4;
}
velX = constrain(velX, -4.0f, 4.0f);
if (ballX < 0 || ballX > W) {
bool leftScored = ballX > W;
if (leftScored) scoreLeft++;
else scoreRight++;
Serial.printf("point to %s, score %d-%d\n", leftScored ? "left" : "right", scoreLeft, scoreRight);
if (scoreLeft == WIN_SCORE || scoreRight == WIN_SCORE) {
Serial.printf("%s wins %d-%d\n", scoreLeft > scoreRight ? "left" : "right", scoreLeft, scoreRight);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 24);
display.print(scoreLeft > scoreRight ? "LEFT WINS" : "RIGHT WINS");
display.display();
delay(3000);
scoreLeft = scoreRight = 0;
}
serve(leftScored ? -1 : 1);
delay(500);
}
display.clearDisplay();
for (int y = 0; y < H; y += 6) display.drawFastVLine(W / 2, y, 3, SSD1306_WHITE);
display.fillRect(1, (int)leftY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.fillRect(W - 1 - PADDLE_W, (int)rightY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(W / 2 - 18, 0);
display.print(scoreLeft);
display.setCursor(W / 2 + 13, 0);
display.print(scoreRight);
display.display();
delay(16); // about 60 frames a second
}Wiring
| From | To board | Note |
|---|---|---|
| OLED VCC | 3V3 | |
| OLED GND | GND | |
| OLED SDA | GPIO 8 | |
| OLED SCL | GPIO 9 | |
| Left knob, middle pin | GPIO 1 | Outer pins of each knob to 3V3 and GND, never 5V |
| Right knob, middle pin | GPIO 2 | Skip it and set TWO_PLAYERS to false to play the computer |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32S3 Dev Module
// Pong on a 128 x 64 SSD1306 with two potentiometers as paddles.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int KNOB_LEFT = 1; // ADC1 pins
const int KNOB_RIGHT = 2;
const bool TWO_PLAYERS = true; // false: the right paddle plays itself
const int WIN_SCORE = 7;
const int W = 128, H = 64, PADDLE_H = 14, PADDLE_W = 3;
Adafruit_SSD1306 display(W, H, &Wire, -1);
float ballX, ballY, velX, velY;
float leftY = 25, rightY = 25;
int scoreLeft = 0, scoreRight = 0;
void serve(int direction) {
ballX = W / 2;
ballY = H / 2;
velX = 1.6f * direction;
velY = random(-10, 11) / 10.0f;
}
float knobToY(int pin, float current) {
float target = analogRead(pin) * (H - PADDLE_H) / 4095.0f;
return current + (target - current) * 0.5f; // a little smoothing
}
void setup() {
Serial.begin(115200);
Wire.begin(8, 9);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 not found at 0x3C");
for (;;) delay(1000);
}
analogReadResolution(12);
randomSeed(esp_random());
serve(1);
Serial.printf("Pong ready. Left paddle: GPIO %d, right paddle: %s\n", KNOB_LEFT, TWO_PLAYERS ? "knob" : "computer");
}
void loop() {
leftY = knobToY(KNOB_LEFT, leftY);
if (TWO_PLAYERS) {
rightY = knobToY(KNOB_RIGHT, rightY);
} else {
float aim = ballY - PADDLE_H / 2;
rightY += constrain(aim - rightY, -1.3f, 1.3f); // beatable on purpose
rightY = constrain(rightY, 0.0f, (float)(H - PADDLE_H));
}
ballX += velX;
ballY += velY;
if (ballY <= 0 || ballY >= H - 2) velY = -velY;
// Paddles: the bounce angle depends on where the ball hits.
if (velX < 0 && ballX <= 4 && ballY + 2 >= leftY && ballY <= leftY + PADDLE_H) {
velX = -velX * 1.05f;
velY = (ballY - (leftY + PADDLE_H / 2)) / 4;
}
if (velX > 0 && ballX >= W - 6 && ballY + 2 >= rightY && ballY <= rightY + PADDLE_H) {
velX = -velX * 1.05f;
velY = (ballY - (rightY + PADDLE_H / 2)) / 4;
}
velX = constrain(velX, -4.0f, 4.0f);
if (ballX < 0 || ballX > W) {
bool leftScored = ballX > W;
if (leftScored) scoreLeft++;
else scoreRight++;
Serial.printf("point to %s, score %d-%d\n", leftScored ? "left" : "right", scoreLeft, scoreRight);
if (scoreLeft == WIN_SCORE || scoreRight == WIN_SCORE) {
Serial.printf("%s wins %d-%d\n", scoreLeft > scoreRight ? "left" : "right", scoreLeft, scoreRight);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 24);
display.print(scoreLeft > scoreRight ? "LEFT WINS" : "RIGHT WINS");
display.display();
delay(3000);
scoreLeft = scoreRight = 0;
}
serve(leftScored ? -1 : 1);
delay(500);
}
display.clearDisplay();
for (int y = 0; y < H; y += 6) display.drawFastVLine(W / 2, y, 3, SSD1306_WHITE);
display.fillRect(1, (int)leftY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.fillRect(W - 1 - PADDLE_W, (int)rightY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(W / 2 - 18, 0);
display.print(scoreLeft);
display.setCursor(W / 2 + 13, 0);
display.print(scoreRight);
display.display();
delay(16); // about 60 frames a second
}Wiring
| From | To board | Note |
|---|---|---|
| OLED VCC | 3V3 | |
| OLED GND | GND | |
| OLED SDA | GPIO 6 | |
| OLED SCL | GPIO 7 | |
| Left knob, middle pin | GPIO 0 | Outer pins of each knob to 3V3 and GND, never 5V |
| Right knob, middle pin | GPIO 1 | Skip it and set TWO_PLAYERS to false to play the computer |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32C3 Dev Module
// Pong on a 128 x 64 SSD1306 with two potentiometers as paddles.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int KNOB_LEFT = 0; // ADC1 pins
const int KNOB_RIGHT = 1;
const bool TWO_PLAYERS = true; // false: the right paddle plays itself
const int WIN_SCORE = 7;
const int W = 128, H = 64, PADDLE_H = 14, PADDLE_W = 3;
Adafruit_SSD1306 display(W, H, &Wire, -1);
float ballX, ballY, velX, velY;
float leftY = 25, rightY = 25;
int scoreLeft = 0, scoreRight = 0;
void serve(int direction) {
ballX = W / 2;
ballY = H / 2;
velX = 1.6f * direction;
velY = random(-10, 11) / 10.0f;
}
float knobToY(int pin, float current) {
float target = analogRead(pin) * (H - PADDLE_H) / 4095.0f;
return current + (target - current) * 0.5f; // a little smoothing
}
void setup() {
Serial.begin(115200);
Wire.begin(6, 7);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 not found at 0x3C");
for (;;) delay(1000);
}
analogReadResolution(12);
randomSeed(esp_random());
serve(1);
Serial.printf("Pong ready. Left paddle: GPIO %d, right paddle: %s\n", KNOB_LEFT, TWO_PLAYERS ? "knob" : "computer");
}
void loop() {
leftY = knobToY(KNOB_LEFT, leftY);
if (TWO_PLAYERS) {
rightY = knobToY(KNOB_RIGHT, rightY);
} else {
float aim = ballY - PADDLE_H / 2;
rightY += constrain(aim - rightY, -1.3f, 1.3f); // beatable on purpose
rightY = constrain(rightY, 0.0f, (float)(H - PADDLE_H));
}
ballX += velX;
ballY += velY;
if (ballY <= 0 || ballY >= H - 2) velY = -velY;
// Paddles: the bounce angle depends on where the ball hits.
if (velX < 0 && ballX <= 4 && ballY + 2 >= leftY && ballY <= leftY + PADDLE_H) {
velX = -velX * 1.05f;
velY = (ballY - (leftY + PADDLE_H / 2)) / 4;
}
if (velX > 0 && ballX >= W - 6 && ballY + 2 >= rightY && ballY <= rightY + PADDLE_H) {
velX = -velX * 1.05f;
velY = (ballY - (rightY + PADDLE_H / 2)) / 4;
}
velX = constrain(velX, -4.0f, 4.0f);
if (ballX < 0 || ballX > W) {
bool leftScored = ballX > W;
if (leftScored) scoreLeft++;
else scoreRight++;
Serial.printf("point to %s, score %d-%d\n", leftScored ? "left" : "right", scoreLeft, scoreRight);
if (scoreLeft == WIN_SCORE || scoreRight == WIN_SCORE) {
Serial.printf("%s wins %d-%d\n", scoreLeft > scoreRight ? "left" : "right", scoreLeft, scoreRight);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 24);
display.print(scoreLeft > scoreRight ? "LEFT WINS" : "RIGHT WINS");
display.display();
delay(3000);
scoreLeft = scoreRight = 0;
}
serve(leftScored ? -1 : 1);
delay(500);
}
display.clearDisplay();
for (int y = 0; y < H; y += 6) display.drawFastVLine(W / 2, y, 3, SSD1306_WHITE);
display.fillRect(1, (int)leftY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.fillRect(W - 1 - PADDLE_W, (int)rightY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(W / 2 - 18, 0);
display.print(scoreLeft);
display.setCursor(W / 2 + 13, 0);
display.print(scoreRight);
display.display();
delay(16); // about 60 frames a second
}Parts
| Quantity | Part | Where to buy |
|---|---|---|
| 1× | ESP32 dev board | Buy at Amazon |
| 1× | 0.96 inch SSD1306 OLED, I2C, address 0x3C | |
| 2× | 10 kΩ potentiometers | Buy at Adafruit |
| 10× | Jumper wires |
Amazon buy links may include an Associates tag, and this site may earn a commission from qualifying Amazon purchases at no extra cost to you. Adafruit links are direct.
Libraries
Adafruit SSD1306, Adafruit GFX Library
Load it
- In the Arduino IDE, install the esp32 board package and pick the board named above the sketch.
- Install the libraries listed above from the Library Manager.
- Paste the sketch, choose the board's port and upload. Hold BOOT if the upload stalls at Connecting.
- Close the IDE's serial monitor, then open the bench here to watch the output.
Field notes
- The knobs are on ADC1 pins. On the classic ESP32 those are GPIO 32 to 39; GPIO 34 and 35 are input only, which is all a knob needs.
- Wire the knobs' outer pins to 3V3 and GND. Using 5V would put 5 V on the GPIO when the knob is turned all the way.
- No second knob? Set TWO_PLAYERS to false and the right paddle plays itself. It is slow on purpose, so you can win.
- If a paddle jitters, add a 100 nF capacitor from the knob's middle pin to GND.
Not tested yet
- Not tested on hardware yet. Compiles for every board listed with Arduino-ESP32 core 3.3.12.
Built it? Send a build report with your board and what the serial port printed. Working reports become the "Tested on this board" entries above.