Robot Faces — 2.1" Smartwatch Kit
This kit is the 1.2" Smartwatch kit built on a bigger panel — a 360 × 360 round color display driven by a GC9B72 controller, instead of the smaller kit's 240 × 240 GC9A01. Every lab number means the same thing in both kits, so they can be taught side by side, and the differences between them (mostly: a wire that got swapped, and a font that doesn't scale the way you'd expect) are worth knowing before you start.
The display comes from this AliExpress listing, for about $6 — GalaxyCore has never published an official datasheet for the GC9B72, so building a driver for it meant reverse-engineering one from the nearest public reference. See Datasheet below.
Three Facts That Drive Everything
Read these before the first lab. They are the same three facts the 1.2" kit is built around — this panel just makes each one bigger.
| Fact | Consequence |
|---|---|
| It is round. The controller addresses a 360 × 360 square; the glass is the circle inside it. | A corner pixel is real, addressable, and permanently invisible — with no error message. |
| It is color. A pixel is a 16-bit RGB565 number, not a 0 or a 1. | Two bytes per pixel instead of one bit — sixteen times the memory of the OLED kit. |
There is no frame buffer, so there is no show(). |
Every drawing call goes straight down the SPI wire, and costs real time — 2.25× the pixels of the 1.2" kit. |
That third one is the big one, on this kit more than any other in the book. A full-screen wipe here is 259,200 bytes down the wire — the single most expensive call you can make — which is why animations erase only the box that changed, starting at lab 11 rather than waiting for lab 29.
A Fourth Fact, Unique to This Kit: Text Doesn't Scale
The bitmap fonts are fixed 8×16 and 16×32 glyphs — they cannot get bigger just because the screen did. A label that covered 3.3% of the 1.2" kit's width covers only 2.2% of this one.
So face.label() draws in the 16×32 font here, where the 1.2" kit uses 8×16 — and that
one change had a consequence worth knowing before you write your own lesson: text() paints a
background behind every glyph, so a label is a solid block as tall as the font, not just some
letters. Doubling the font doubled that block to 32 rows, and on the first pass it was tall enough
to paint over the top of a raised eyebrow. face.LABEL_Y was moved from the naive scaled value to
32 to clear it. If you write a lesson that positions a label near the top of the face, check it
against the tallest eyebrow in the kit, not just against the circle.
Getting Started
| Lesson | What you'll learn |
|---|---|
| Connection Test | Blink GP25 and prove the board is alive before you wire anything |
| Hello World | Confirm the display works, and that text() needs a font module |
| Screen Coordinates | The coordinate system — and that the corners are not there |
The Drawing Primitives
These seven lessons cover every drawing command you will need. Work through them in order and you will have the complete toolkit.
| Lesson | Command |
|---|---|
| Pixel | pixel() — the single dot, and why one call per dot is expensive here |
| Lines | hline(), vline(), line(), and the eyebrow rule |
| Rectangle | rect() vs fill_rect(), and erasing with black |
| Ellipse | shapes.ellipse() and the quadrant fill codes |
| Circle | Circles, and ring() — the shape a round screen was made for |
| Polygon | shapes.poly() and the scanline fill behind it |
| Blit | blit_buffer(), RGB565 sprite memory, and keyed transparency |
Building Faces
| Lesson | What you'll learn |
|---|---|
| Your First Face | The first complete expression: eyes + eyebrows + mouth |
| Eye Scanner | Animating a pupil sweep by erasing only the eye boxes |
| Making the Eye Scanner Fast | Measuring lab 11 on real hardware and taking it from 455 ms to 4.2 ms per frame |
| Winking with a Smile | A closed-eye arc on just one eye |
| Blinking | Reading a button with debounce, and closing both eyes |
| Eyebrows | Curved eyebrows built from poly() |
| Don't Block the Loop | Pacing with ticks_ms() — and how a slow draw blocks too |
| Sleeping Face | Closed eyes, drooping brows, and a drifting Zzz |
Buttons, Menus, and Demos
| Lesson | What you'll learn |
|---|---|
| Reading Two Buttons | Two buttons independently, and why text has to be erased first |
| Mode Switching | Button A and B cycle forward and back through a list |
| The Expression Menu | A two-button menu over all seven Ekman emotions |
| Demo Reel | A self-running showcase, no buttons needed |
| Standalone main.py | Demo reel + button menu, meant to become main.py |
| Live Face Parameters | Tuning one face parameter live with two buttons |
| Color and Resolution Demo | Six test patterns built for this kit alone — the first thing to run on brand-new hardware |
Thinking About Your Code
The lessons above teach you how to make the hardware do something. These eleven teach you how to think about the code you just wrote — the four habits that transfer to every program you will ever write, taught on code you already understand.
Work them in order, and only after you have finished the lessons above.
| Lesson | Thinking skill | What you'll learn |
|---|---|---|
| The Face Module | Decomposition, abstraction | Move the duplicated face parts into one shared file |
| The Emotion Table | Pattern recognition | Seven emotions become seven rows of data — then color arrives as one more column |
| Five Broken Faces | Debugging | Five planted bugs, a method for finding them, and a symptom table |
| Trace and Watch | Debugging by measurement | An on-screen instrument panel for bugs you cannot photograph |
| Keyframes | Algorithms | An animation is a list of poses, and one player runs them all |
| A Face With a Memory | Abstraction, modeling | States and transitions as tables, instead of tangled if-statements |
| Only Redraw What Changed | Decomposition, measurement | Redraw just the moving part, color the boxes to see it, then measure |
| Design Your Own Emotion | All four | Invent an expression and test whether a stranger can read it |
| Eye Saccade | Modeling | A pupil that darts and settles, the way a real eye does |
| How Fast Is a Face? | Measurement, algorithms | Race pixel-at-a-time drawing against row runs |
| Color and Bits | Representation | RGB565 taken apart: masking, shifting, and what 16.7M colors lose |
| The Color Wheel | Measurement, optimization | Every color at once, and a question the smaller kit's version couldn't ask |
What's in the Kit
- Raspberry Pi Pico
- 2.1" GC9B72 round display module, 360 × 360
- Half-size solderless breadboard (400 tie points)
- Ten-wire M-F Dupont cable
- Two momentary push buttons
Wiring
The 10-pad breakout reads, left to right: GND VCC SDA SCL RST DC CS BL SDO TE. Only 8 of those
are wired — SDO (read-back) and TE (frame-sync) are not used by this driver.
| Module pin | Pico pin | Wire color |
|---|---|---|
| SCL / CLK | GP2 | orange |
| SDA / MOSI | GP3 | yellow |
| RST | GP4 | green |
| DC | GP5 | blue |
| CS | GP6 | purple |
| BL | GP7 | gray |
| VCC | 3V3 | red |
| GND | GND | black |
| Button A | GP14 (PULL_UP, other leg to GND) | |
| Button B | GP15 (PULL_UP, other leg to GND) |
Buttons A and B are on GP14 and GP15 in every kit in this book, so wiring habits carry across when you swap displays. The three pins that matter for the display, though, do not carry across unchanged: the 1.2" kit's GC9A01 puts DC/CS/RST on GP4/5/6 in that order, and this kit's GC9B72 puts RST/DC/CS on the same three pins in a different order. Copying one kit's wiring notes onto the other's breadboard will not work — check the table above, not your memory of the other kit.
One backlight difference worth knowing: on the 1.2" kit's default wiring, BL is tied straight to
3V3 and config.set_backlight() is a no-op. On this kit BL is a real GPIO (GP7), so
config.set_backlight(True) genuinely does something — which also means a backlight left low by
mistake is a real way to get a "dead" screen here.
Full wiring notes, upload instructions, and troubleshooting are in the kit's README.
Porting Cheat Sheet
For anyone bringing 1.2" kit code across, or teaching the two kits together:
| 1.2" Smartwatch (GC9A01, 240×240) | 2.1" Smartwatch (GC9B72, 360×360) |
|---|---|
| Screen is 240 × 240 | Screen is 360 × 360 — every position and size is the 1.2" kit's number × 1.5 |
face.label() uses the 8×16 font |
face.label() uses the 16×32 font — see above |
config.SAFE_RADIUS = 112 |
config.SAFE_RADIUS = 168 — an estimate, not yet measured against this panel |
| DC/CS/RST on GP4/5/6, in that order | RST/DC/CS on GP4/5/6 — same pins, different roles |
BL tied to 3V3, set_backlight() is a no-op |
BL on GP7, set_backlight() really switches it |
lib/gc9a01.py |
lib/gc9b72.py — a new driver; no public datasheet exists for this chip |
Lessons Unique to This Kit
- Making the Eye Scanner Fast — a step-by-step optimization story: how timing code inside the program found the slow part, why the obvious fix made almost no difference, and how the eye scanner went from 455 ms per frame to 4.2 ms.
- Color and Resolution Demo — six test patterns built to bring up brand-new GC9B72 hardware, with no lab-11-style twin on the smaller kit.
References

Datasheet
GalaxyCore has never published an official public datasheet for the GC9B72. The closest thing that exists is the register-level init sequence in this reference driver, which is what our own MicroPython driver was ported from:
- xboot/xstar
fb-gc9b72.c-- a Linux framebuffer driver, and (per the credit below) "the only known-good public GC9B72 init" anyone has found. - MaliosDark/Arduino_GC9B72 --
an Arduino_GFX driver that ports the same init sequence to C++, and
documents the panel's silkscreen ID (
VER:TFT 2.1 0_10,Driver IC: GC9B72,Resolution: 360x360) and pinout.