Arduino Tutorials

Pimoroni Badger 2350 in the Browser: E-Paper, Buttons, and BadgeOS

Pimoroni’s Badger 2350 isn’t a typical dev board. It’s a badge: e-paper screen, five buttons, a battery, and firmware that ships with a to-do list, a pomodoro timer, and a raycaster game. The goal for this simulator was simple: make the badge in a browser tab feel like the one on your lanyard, buttons included.

This is part of a series on hardware partners. Pimoroni’s entry is the first built around e-paper, and that changes everything. Nothing animates at 60 fps here — the screen refreshes like real ink, and whatever you draw stays put.

Wiring and the board

The Badger 2350 uses an RP2350, with a 264×176 SSD1680 e-paper panel. Here’s the pin mapping you’ll care about:

  • UP: GPIO 18, active-low (pull-up inside)
  • DOWN: GPIO 19, active-low
  • A: GPIO 20, active-low
  • B: GPIO 21, active-low
  • C: GPIO 22, active-low
  • E-paper: SPI on GPIO 2 (SCK), 3 (MOSI), 4 (CS), 5 (DC), 6 (RST), 7 (BUSY)
  • Case LEDs: GPIO 0-3 (four LEDs on the back)

The buttons are the point. In the simulator, clicking them does the same GPIO reads a physical badge performs — pull-ups, active-low behavior, the works.

The sketch

Here’s the to-do list example, written as you’d write it after reading the schematic:

#include <Arduino.h>
#include <SSD1680.h> // e-paper driver

// Pin definitions
#define BTN_UP 18
#define BTN_DOWN 19
#define BTN_A 20
#define BTN_B 21
#define BTN_C 22
#define LED1 0
#define LED2 1
#define LED3 2
#define LED4 3

SSD1680 display(264, 176);

const char* tasks[] = {"Buy milk", "Write blog", "Walk dog"};
bool done[3] = {false, false, false};
int cursor = 0;

void setup() {
  pinMode(BTN_UP, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);
  pinMode(BTN_A, INPUT_PULLUP);
  pinMode(BTN_B, INPUT_PULLUP);
  pinMode(BTN_C, INPUT_PULLUP);
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
  pinMode(LED4, OUTPUT);
  display.begin();
  drawUI();
}

void loop() {
  if (digitalRead(BTN_UP) == LOW) {
    cursor = (cursor + 2) % 3; // move up
    drawUI();
    delay(200);
  }
  if (digitalRead(BTN_DOWN) == LOW) {
    cursor = (cursor + 1) % 3; // move down
    drawUI();
    delay(200);
  }
  if (digitalRead(BTN_A) == LOW) {
    done[cursor] = !done[cursor]; // toggle task
    updateLEDs();
    drawUI();
    delay(200);
  }
  if (digitalRead(BTN_B) == LOW) {
    for (int i = 0; i < 3; i++) done[i] = false; // clear all
    updateLEDs();
    drawUI();
    delay(200);
  }
  if (digitalRead(BTN_C) == LOW) {
    for (int i = 0; i < 3; i++) done[i] = true; // tick all
    updateLEDs();
    drawUI();
    delay(200);
  }
}

void drawUI() {
  display.clear();
  for (int i = 0; i < 3; i++) {
    int y = 20 + i * 40;
    if (i == cursor) display.fillRect(0, y-5, 264, 30, SSD1680_BLACK);
    display.setTextColor(i == cursor ? SSD1680_WHITE : SSD1680_BLACK);
    display.setCursor(10, y);
    display.print(done[i] ? "[x] " : "[ ] ");
    display.println(tasks[i]);
    if (done[i]) display.drawLine(10, y+10, 254, y+10, SSD1680_BLACK); // strikethrough
  }
  display.update();
}

void updateLEDs() {
  int count = 0;
  for (int i = 0; i < 3; i++) if (done[i]) count++;
  digitalWrite(LED1, count > 0);
  digitalWrite(LED2, count > 1);
  digitalWrite(LED3, count > 2);
  digitalWrite(LED4, count > 3); // never true with 3 tasks, but okay
}

How it works

The sketch reads the buttons with internal pull-ups, so a LOW means pressed. The e-paper is driven via SPI, and the display update is full-refresh — that’s why you see the flash when toggling a task. The case LEDs reflect progress: one LED per completed task, up to three.

In the simulator, every GPIO read goes through the same emulated pull-ups, so the logic is identical to hardware. The e-paper refresh timing is also emulated — you get that characteristic flicker.

What the simulator won’t tell you

Honest notes: the simulator doesn’t model the e-paper’s ghosting perfectly. Real e-paper leaves faint shadows of previous content; the simulator does a clean refresh. Also, the SSD1680’s partial refresh mode — which the hydration counter example uses — is emulated, but the exact waveform timing is simplified. On real hardware, partial refresh can be finicky; the sim makes it look easy.

The buttons are another gap. Real buttons have bounce; you’d need debouncing in production code. The simulator’s buttons are clean, so your sketch might work in the browser but fail on the bench if you skip debouncing. The LED brightness isn’t accurate either — real LEDs have current limits; the sim just turns them on.

When it doesn’t work

  • Nothing happens when you click a button → Check the pin numbers. If you wired to the wrong GPIO, the sim won’t know.
  • E-paper shows garbage → SPI wiring issue. Verify SCK, MOSI, CS, DC, RST, BUSY.
  • Sketch compiles but screen stays blank → Did you call display.begin()? Also check if you’re using the right display driver.
  • LEDs don’t light → Are you setting them as OUTPUT? And are you writing HIGH/LOW correctly?

Next steps

Try the BadgeOS firmware in the simulator — it’s the factory firmware running unmodified. Or play with the MicroPython REPL. For a challenge, write a name badge app or an RTC alarm clock.

Open the simulator and give it a spin.

Leave a Reply

Your email address will not be published. Required fields are marked *