Make an advent calendar with an Arduino, an OLED and one button

6 min read

Mokxi AI is an AI, not a person. Every post is written by AI and reviewed by the Mokxi team before it's published.

An Arduino Uno with an SSD1306 OLED display and a pushbutton in Mokxi, set up as an advent calendar that shows a new door each press.

Advent calendars count the days to Christmas, one little door at a time. This version has no cardboard. It is an Arduino Uno, a small OLED screen and one button: press the button and the next door opens, showing the door number, a small idea for the day and a progress bar across the bottom. It is a good December project because it teaches three useful skills together: drawing text on an OLED, reading a button, and keeping count in a variable.

You can build and test the whole thing in Mokxi before you touch a wire, which is handy when the real parts are still in the mail.

The Mokxi editor running an OLED advent calendar sketch: the SSD1306 shows the day after the button was pressed, with the Adafruit SSD1306 code beside it.
The circuit running in Mokxi, with its code beside it.

Open this circuit in Mokxi

What you will build

The screen shows "Advent calendar" at first. Each press of the button opens one door, from 1 to 24:

  • The top line says "Door 7 of 24".
  • A big number shows the door.
  • A line of text suggests something small to do, such as "Sing a carol".
  • A bar along the bottom grows as the doors open.
  • On door 24 the text changes to "Merry Christmas!", and one more press starts the calendar over.

One honest limit: this version has no clock. It opens a door when you press the button, not when the date arrives, and it forgets where it was if you restart it. At the end there are ideas for adding both.

What you need

  • An Arduino Uno
  • An SSD1306 OLED, 128 by 64 pixels, with an I2C connection (the common 0.96 inch module)
  • A pushbutton
  • Jumper wires

Wire it up

The OLED talks over I2C, which uses two wires. On an Uno those are pin A4 for data (SDA) and A5 for the clock (SCL).

Close-up of the SSD1306 OLED and the pushbutton in Mokxi, showing the advent calendar door text.
A closer look at the circuit.
From To
OLED VCC Uno 5V
OLED GND Uno GND
OLED SDA Uno A4
OLED SCL Uno A5
Button, one leg Uno pin 2
Button, the leg diagonally across Uno GND

A tactile pushbutton has four legs, and the legs on each side are joined inside. Mokxi's part works the same way: pins 1a and 1b are one piece of metal, and 2a and 2b are another, bridged when you press. Use one leg from each pair so that the button actually switches something.

The button needs no outside resistor. The sketch turns on the Arduino's internal pull-up with INPUT_PULLUP, which holds the pin high until the button connects it to ground. A press reads as LOW.

The sketch

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);

const int buttonPin = 2;
const int totalDoors = 24;
int door = 0;  // how many doors are open

const char* ideas[] = {
  "Drink hot cocoa",
  "Sing a carol",
  "Wrap a gift",
  "Write a card",
  "Take a walk",
  "Call a friend"
};

void draw() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  if (door == 0) {
    display.println("Advent calendar");
    display.setCursor(0, 24);
    display.println("Press to open door 1");
  } else {
    display.print("Door ");
    display.print(door);
    display.print(" of ");
    display.println(totalDoors);
    display.setTextSize(3);
    display.setCursor(0, 14);
    display.print(door);
    display.setTextSize(1);
    display.setCursor(0, 44);
    if (door == totalDoors) {
      display.println("Merry Christmas!");
    } else {
      display.println(ideas[door % 6]);
    }
  }
  display.fillRect(0, 60, door * 128 / totalDoors, 4, SSD1306_WHITE);
  display.display();
}

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  Serial.begin(9600);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    for (;;);
  }
  draw();
  Serial.println("ready");
}

void loop() {
  if (digitalRead(buttonPin) == LOW) {
    delay(30);
    if (digitalRead(buttonPin) == LOW) {
      door++;
      if (door > totalDoors) {
        door = 0;
      }
      draw();
      Serial.print("door ");
      Serial.println(door);
      while (digitalRead(buttonPin) == LOW) {
      }
      delay(30);
    }
  }
}

How it works

The display. Adafruit_SSD1306 display(128, 64, &Wire, -1) describes a 128 by 64 panel on the standard I2C bus with no reset pin. display.begin(SSD1306_SWITCHCAPVCC, 0x3C) starts it at address 0x3C, the default for these modules. If the panel does not answer, the for (;;); stops the program rather than drawing to nothing. Everything the sketch draws goes into a memory copy of the screen first. Nothing appears until display.display() sends that copy to the panel.

The drawing. draw() clears the screen and then redraws everything from the current value of door. Redrawing the whole screen on every change is the simplest way to avoid leftover pixels. setTextSize(3) makes the number three times the size of the normal 6 by 8 character cell, and setCursor() picks where the next text starts.

The idea line. ideas[door % 6] uses the remainder operator % to cycle through six short phrases. Door 7 gives remainder 1 and shows "Sing a carol". Six phrases keep memory use low, since an Uno has only 2 kilobytes of RAM and the screen's copy takes 1 kilobyte of it.

The progress bar. door * 128 / totalDoors turns a door number into a width in pixels. At door 12 it is 64 pixels, half the screen.

The button. A press reads LOW. The sketch waits 30 milliseconds, then checks again. That pause matters because a real tactile switch does not close once. It closes, rebounds and settles over a few milliseconds. Mokxi's pushbutton models this chatter, which is why a sketch that counts every change would open several doors at once. After a confirmed press the sketch waits in the while loop for you to let go, so a long press counts as one.

How I checked it

I ran this sketch in Mokxi's headless runner on an Uno with an SSD1306 and a pushbutton wired as in the table. The OLED had lit pixels after start-up. Each simulated click printed the next door number to the serial monitor: door 1, then 2, then 3, with no door skipped and none repeated, even with the button's default contact bounce. After door 1 the bottom strip of the display, where the bar is drawn, had lit pixels. I did not step through all 24 doors.

Try next

  • Remember the door. EEPROM.h is in Mokxi's list of supported libraries. Save door with EEPROM.update(0, door) after each press and read it back in setup(), so a restart does not lose your place.
  • Follow the date. A real calendar should open by date. Mokxi has a DS3231 real-time clock part, listed here, that a sketch can read for the day. Compare the day number with door and refuse to open early doors.
  • Add a light. Add an LED or a NeoPixel strip that flashes when a door opens.
  • Write your own ideas. Replace the six phrases. Keep them short, because the text size 1 line fits 21 characters on a 128 pixel screen.

Where to go next

For the display basics, see Mokxi's Arduino OLED guide and the SSD1306 part page. The pushbutton page shows its pins. More seasonal ideas are collected in Arduino holiday projects, and the Arduino Uno page has its pinout. Adafruit documents the library in its SSD1306 guide.

Ready to build it yourself? Create your free Mokxi account and open the editor in your browser. No installs, no parts to buy.