Skip to content

Project: Etch-a-Sketch

Ever used an Etch-a-Sketch? Now you'll build a digital one. Two dials move a pen around the OLED screen, one button lifts the pen so you can move without drawing, and another shakes the screen clean.

The Etch-a-Sketch drawing on the OLED

Photo from the first edition. Follow the wiring table for pin numbers.

Objective

  • Potentiometer 1 moves the pen left and right.
  • Potentiometer 2 moves the pen up and down.
  • Button A lifts the pen up or puts it back down. While it's up, a blinking dot shows where you are.
  • Button B clears the screen.

You'll need:

  • 1 × SSD1306 OLED screen (128 × 64, I2C)
  • 2 × potentiometers
  • 2 × push buttons
  • Jumper wires

Before you start

This project builds on OLED Screens, Potentiometers and Buttons. Make sure button.py and ssd1306.py are saved on your ESP32 (see Saving Programs to the Board).

Wiring

This project needs pins on both sides of the ESP32. Clip two breadboards together and sit the ESP32 across the gap, so both rows of pins have free holes next to them:

An ESP32 straddling two breadboards

Part Pin Connects to
OLED GND GND
VCC 3V3
SCL GPIO 22
SDA GPIO 21
Potentiometer 1 (X) outer legs 3V3 and GND
middle leg GPIO 34
Potentiometer 2 (Y) outer legs 3V3 and GND
middle leg GPIO 35
Button A (pen) one leg GPIO 14, other leg to GND
Button B (clear) one leg GPIO 25, other leg to GND

Tip

If a dial moves the pen the "wrong" way, swap the 3V3 and GND wires on its two outer legs. Nothing in the code needs to change.

Plan it out

  1. Set up each component: two ADC inputs, two buttons and the OLED screen over I2C.
  2. Break the job into functions.
    • read_smooth(pot) averages a handful of readings to steady the pen.
    • read_position() turns the two dial readings into an (x, y) point on the screen.
    • show_cursor(), hide_cursor() and blink_cursor() handle the blinking dot while the pen is up.
  3. Main loop: check the buttons, read the dials, and draw a line from the last point to the new one.

Code

Save this to your ESP32 as main.py, or click Run in Thonny.

etch_a_sketch.py
# Etch-a-Sketch: draw on the OLED screen with two potentiometers.
# Pot 1 moves left/right, Pot 2 moves up/down.
# Button A lifts or lowers the pen, Button B clears the screen.
import time
from machine import Pin, ADC, I2C
import ssd1306
from button import Button

POT_X_PIN = 34    # Potentiometer 1
POT_Y_PIN = 35    # Potentiometer 2
PEN_BUTTON_PIN = 14    # Button A
CLEAR_BUTTON_PIN = 25  # Button B
SDA_PIN = 21
SCL_PIN = 22

WIDTH = 128
HEIGHT = 64

# ---- set up the parts ----
pot_x = ADC(Pin(POT_X_PIN), atten=ADC.ATTN_11DB)
pot_y = ADC(Pin(POT_Y_PIN), atten=ADC.ATTN_11DB)
pen_button = Button(PEN_BUTTON_PIN)
clear_button = Button(CLEAR_BUTTON_PIN)
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN))
display = ssd1306.SSD1306_I2C(WIDTH, HEIGHT, i2c)

pen_down = True
cursor_shown = False    # is the blinking cursor currently drawn?
cursor_under = 0        # the pixel that was under the cursor
last_blink = time.ticks_ms()


def read_smooth(pot):
    # average 16 readings to stop the line from jittering
    total = 0
    for _ in range(16):
        total += pot.read_u16()
    return total // 16


def read_position():
    x = read_smooth(pot_x) * (WIDTH - 1) // 65535
    # (0, 0) is the TOP left of the screen, so flip y:
    # turning the dial up moves the pen up
    y = (HEIGHT - 1) - read_smooth(pot_y) * (HEIGHT - 1) // 65535
    return x, y


def show_cursor(x, y):
    # draw the cursor as the opposite of whatever pixel is underneath
    global cursor_shown, cursor_under
    cursor_under = display.pixel(x, y)
    display.pixel(x, y, 1 - cursor_under)
    cursor_shown = True


def hide_cursor(x, y):
    # put back the pixel that was under the cursor
    global cursor_shown
    if cursor_shown:
        display.pixel(x, y, cursor_under)
        cursor_shown = False


def blink_cursor(x, y):
    # while the pen is up, flash one pixel so you can see where you are
    global last_blink
    if time.ticks_diff(time.ticks_ms(), last_blink) > 300:
        last_blink = time.ticks_ms()
        if cursor_shown:
            hide_cursor(x, y)
        else:
            show_cursor(x, y)
        display.show()


display.fill(0)
display.show()
last_x, last_y = read_position()

while True:
    if clear_button.was_pressed():
        display.fill(0)
        display.show()
        cursor_shown = False

    if pen_button.was_pressed():
        hide_cursor(last_x, last_y)
        pen_down = not pen_down
        print("Pen down" if pen_down else "Pen up")

    x, y = read_position()

    if pen_down:
        if (x, y) != (last_x, last_y):
            display.line(last_x, last_y, x, y, 1)
            display.show()
    else:
        if (x, y) != (last_x, last_y):
            hide_cursor(last_x, last_y)   # the cursor moves with the dials
            display.show()
        blink_cursor(x, y)

    last_x, last_y = x, y
    time.sleep_ms(5)

How it works

From dial to pixel

The screen is 128 pixels wide and 64 tall. Just like in the Music Machine, the code maps each dial's 0–65535 reading onto that range:

x = read_smooth(pot_x) * (WIDTH - 1) // 65535

On the OLED, (0, 0) is the top-left corner, and y gets bigger as you go down. That feels backwards when you're turning a dial, so the code flips it with (HEIGHT - 1) - .... Turning Potentiometer 2 up moves the pen up.

Drawing lines, not dots

If the dial moves quickly, the pen can jump several pixels between two readings. Drawing single dots would leave gaps, so instead the code draws a line from the previous point to the new one with display.line(). last_x, last_y = x, y then remembers the new point for next time.

A cursor that doesn't damage your drawing

While the pen is up, the cursor blinks by flipping one pixel. Before drawing it, show_cursor() remembers what was underneath (display.pixel(x, y) with no colour reads a pixel). hide_cursor() puts it back exactly as it was, so the cursor never rubs out any of your lines.

Result

Turn the dials to draw, press Button A to hop to a new spot without leaving a trail, and Button B to start over. You can also press the ESP32's EN (reset) button to clear the screen.

Make it your own

  • Save your art: keep a list of every point you draw and replay the drawing on start-up.
  • Eraser mode: add Button C (GPIO 32) to switch between drawing white (colour 1) and erasing (colour 0).
  • Shape stamps: use display.rect() or display.ellipse() to stamp a shape at the cursor when you press a button.