Skip to content

Robot Faces FAQ

Getting Started Questions

What is the Robot Faces textbook about?

Robot Faces: Drawing Expressive Displays for STEM Robots is a hands-on course that teaches you to program expressive robot faces using small, inexpensive screens. You'll work with two hardware targets: a $20 monochrome OLED display (128x64 pixels) and a $10 color round display (240x240 pixels), both driven by a Raspberry Pi Pico microcontroller running MicroPython.

The core idea is that a robot doesn't need arms, legs, or wheels to seem alive — a screen where its face should be, showing eyes, eyebrows, and a mouth that move, is enough to make people read emotion into a machine. You'll learn to break a face down into independent parts (eyes, pupils, eyebrows, mouth) that combine into recognizable expressions like happy, sad, angry, and surprised, drawing on psychologist Paul Ekman's research on universal human emotions.

The course is deliberately narrow: it focuses entirely on the display, the drawing code, and the design of expression, rather than robot chassis assembly, motors, or navigation. See the Course Description for the full list of topics covered, and About This Book for the story behind why it was written.

Who is this textbook designed for?

This textbook is written primarily for high school students in grades 9-12 who are part of a STEM, robotics, or coding club — no classroom teacher or formal enrollment required. It also works well for advanced middle-school students and CoderDojo-style after-school programs, as long as an adult mentor is available to help with the hardware setup.

You don't need to already know robotics, and you don't need to be great at math or physics. What matters more is curiosity and a willingness to tinker: the course rewards students who like nudging a value and immediately seeing whether an eyebrow looks angrier or a mouth looks happier. Because a complete kit costs under $30, it's realistic for every student in a classroom to build and keep their own robot face rather than share equipment or return it at the end of a unit.

The course is also a reasonable fit for self-directed learners outside a formal club setting, since it's designed for both self-paced study and classroom use. See the Course Description for the complete target-audience and prerequisites statement.

Do I need any prior programming experience to start this course?

No. This course assumes no prior programming experience at all. MicroPython syntax — variables, functions, loops, and how to read and fix an error message — is introduced from scratch early in the book, before any drawing code is shown.

If you've used a block-based language like Scratch before, you'll likely recognize concepts like variables and functions right away, which can make those early lessons feel familiar. But that background is not required — the course is written so a complete beginner can follow it step by step, one new idea at a time.

The one thing that does help, though isn't strictly required, is basic computer literacy: being comfortable using a file manager and editing a plain text file. If you've saved a file, renamed it, or found it again in a folder, you already have what you need.

Programming fundamentals get two full chapters of their own — "MicroPython Fundamentals I" and "MicroPython Fundamentals II" — before the book moves on to drawing shapes on a screen, so you won't be expected to write display code until you understand the language it's written in. See the Course Description for the complete prerequisites statement and the Chapters overview for where those lessons fall in the sequence.

Do I need electronics experience or know how to solder?

No soldering and no prior electronics experience are required. Every circuit in this course is built on a solderless breadboard using push-in jumper wires, so you connect and disconnect components by hand rather than melting metal.

The electronics knowledge you do need — how to identify a pin, how SPI wiring works, what a breadboard's power rails are for — is taught starting in the very first chapter, Hardware & Electronics Foundations. That chapter walks through wiring a display to a Raspberry Pi Pico using five SPI pins (SCL, SDA, DC, RES, CS) before any code is written, so by the time you reach programming, the physical connections are already familiar.

For example, the minimal kit uses M-F (male-to-female) jumper wires in 20cm or 40cm lengths to connect the Pico's pins to the breadboard and then to the display — no crimping, cutting, or soldering tools of any kind. If a wire is in the wrong hole, you simply pull it out and move it.

This hands-off-soldering approach is intentional: it keeps the cost and setup time low enough that a classroom of 20-30 students can each build a working robot face in a single class period rather than a multi-week electronics unit.

What hardware do I need to build a robot face?

The minimal robot face kit has just four parts, all listed on the Getting Started page:

  1. A Raspberry Pi Pico microcontroller
  2. A mini solderless breadboard
  3. A 128x64 OLED display (the small monochrome screen)
  4. A handful of male-to-female jumper wires (20cm or 40cm both work)

Everything connects without soldering: the Pico plugs into the breadboard, and jumper wires carry power and data (SPI) signals from the Pico's pins to the display. Later in the course you'll also work with a $10 color round display (240x240 pixels, GC9A01 driver) to compare monochrome and color designs, and optionally with buttons, a potentiometer, or a rotary encoder for interactive controls.

A complete kit, including the microcontroller, one display, a breadboard, and wires, typically costs under $30 total, with the Raspberry Pi Pico alone costing under $5. The full Parts List includes current sourcing links, price ranges, and notes for ordering multiples for a classroom. Nothing on this list requires 3D printing, a custom circuit board, or specialized tools — everything is off-the-shelf and reusable if you want to build a second face later.

How much does a complete robot face kit cost?

A complete robot face kit costs under $30, which is one of the reasons this course was written the way it was — it's cheap enough that every student in a classroom can build and keep their own kit instead of sharing equipment.

Here's roughly where that money goes: a Raspberry Pi Pico microcontroller costs about $4, a 128x64 monochrome OLED display costs $10-20 depending on current pricing and quantity, a small solderless breadboard costs a few dollars, and a set of jumper wires costs a few dollars more. If you later add the $10 color round display used in later chapters, plus a couple of buttons for interactive controls, you're still well under $50 for a kit that covers the entire course.

Prices vary somewhat with tariffs, shipping method, and how many units you buy at once — ordering from AliExpress or eBay in bulk lots is typically cheaper per unit but takes 3-5 weeks to arrive, while Amazon or MicroCenter cost a bit more but ship in days. The Parts List has current sourcing notes, including tips for outfitting a whole classroom of 10-30 students at once. See About This Book for more on why keeping the cost this low was a deliberate design goal.

How is the Robot Faces textbook organized?

The textbook has several kinds of content, all reachable from the navigation menu. The core content is 16 chapters, covering hardware and wiring, MicroPython fundamentals, coordinate systems and frame buffers, drawing primitives, ellipses and polygons, facial anatomy, emotion psychology, animation, interactive controls, color-display porting, and a final capstone project. Chapters are designed to be read in order, since each one builds on concepts introduced earlier.

Alongside the chapters, the book includes 20 lessons — short, focused walkthroughs of a single drawing or interaction skill, each with sample output so you can check your work — and roughly 48 interactive MicroSims, browser-based simulations you can experiment with directly on the page. There are also 16 chapter quizzes, a Learning Graph that visualizes how all the concepts in the course connect to each other, Stories about the commercial robots that inspired this course, a Glossary of key terms, and a Parts List for ordering hardware.

If you want the full breakdown of what each chapter covers, see the Chapters overview. For a page-by-page tour of every section of the site, see the homepage, which lists Getting Started, Chapters, Lessons, Stories, the Learning Graph, the Glossary, and the Parts List.

How long does it take to complete this course?

There's no single official duration, because the course is designed for both self-paced study and classroom use, and paces differ a lot between a student working alone at home and a coding club meeting once a week. As a rough guide, the book contains 16 chapters, 20 short lessons, and a capstone project, which is roughly the scope of a one-semester elective or a school year's worth of a once-a-week after-school club.

A single class period is usually enough to get through the hardware setup in Getting Started and see your first face appear on the screen — that part is intentionally fast so students get a payoff quickly. The chapters that follow go deeper and take longer: MicroPython fundamentals, drawing primitives, and facial anatomy each take real practice, and the animation and interactive-controls chapters near the end assume you're comfortable with everything before them.

Because later chapters only depend on earlier material in this same book, you can go as slowly as you need on a hard chapter without falling behind on unrelated topics. If you're working independently and moving efficiently, expect several weeks of regular practice to reach the capstone; a supported classroom or club setting with weekly sessions more often spans a semester or a school year.

What software do I need to install before I start?

You need one main tool: the Thonny editor, a free Python IDE designed for beginners and built specifically to work well with MicroPython boards like the Raspberry Pi Pico. Thonny runs on Windows, macOS, and Linux.

Setup has four steps, covered in detail on the Getting Started page:

  1. Download and install Thonny from thonny.org
  2. In Thonny, go to Tools → Options → Interpreter and select the Raspberry Pi Pico option
  3. Click "Install Runtime" to load MicroPython onto your Pico
  4. Press Stop/Reset — you should see a >>> prompt confirming MicroPython is running on the board

Once that prompt appears, you're ready to copy your first program (a file called simple-face.py) onto the Pico and press Thonny's green "Run" button to see a face appear on your display. No account creation, license purchase, or command-line tools are required — Thonny's interface handles connecting to the board, transferring files, and running code, all through simple menus and buttons.

You will need a USB cable to connect the Pico to your computer, which is not always included with the microcontroller, so it's worth checking the Parts List or your own drawer for a compatible one before class starts.

What will I be able to build by the end of the course?

By the end of the course, you'll be able to design and program an original robot face with its own personality — one that displays at least six, and ideally eight or more, distinct recognizable emotional expressions, triggered by button or rotary-encoder input rather than hard-coded to run once and stop.

Along the way you'll build smaller projects that lead up to that goal: a working draw_face() function that redraws a complete face from a small set of parameters (eye size, eyebrow angle, mouth curvature) instead of separate hard-coded pictures for every expression; an idle animation with blinking and gaze movement so the face looks alive even when nothing else is happening; and a menu system, driven by a simple state machine, that lets a user cycle between expressions live.

The course's capstone project asks you to bring all of this together on both the 128x64 monochrome OLED and the 240x240 color round display, and to present it to a class or group along with an explanation of the design choices behind each expression — why a particular eyebrow angle reads as "angry," for instance, rather than "confused."

See the Course Description for the complete list of learning outcomes, organized from basic recall up through this capstone-level creative work.

Should I read the chapters in order, or can I skip around?

Reading in order is strongly recommended. The 16 chapters are sequenced so that each one only depends on concepts introduced in that same chapter or an earlier one — there's no chapter that assumes something you haven't been taught yet, but there's also no guarantee a later chapter will make sense if you skip ahead.

Two pairs of chapters are split into two parts each and should be treated as a single unit: Chapters 3 and 4 cover MicroPython fundamentals (Part I: syntax, data, and loops; Part II: functions and the FrameBuf module), and Chapters 10 and 11 cover emotion design (Part I: emotion theory and the core expression set; Part II: readability and human-robot interaction). Complete both halves of a pair before moving to the next chapter.

If you already know some MicroPython, or just want to check the prerequisites for one specific topic instead of reading straight through, the book's Learning Graph lets you look up any concept and see exactly what it depends on, so you can jump in non-linearly with more confidence than guessing. See the Chapters overview for the full chapter list and which chapters pair together.

What topics are NOT covered in this course?

This course is deliberately narrow, focusing entirely on the screen, the drawing code, and the design of facial expression — not on building a complete robot. Specifically, it does not cover:

  • Physical robot chassis assembly, motor control, wheels, servos, or autonomous navigation
  • Computer vision or camera-based emotion recognition
  • Natural language processing, conversational AI, or voice/speech synthesis
  • Non-display sensors and actuators, such as distance sensors or LED strips, beyond a brief optional mention
  • Custom PCB design or soldering — all wiring uses solderless breadboards and jumper wires
  • 3D-printed or laser-cut enclosure design
  • General-purpose Python topics unrelated to embedded displays, like data science or web development

The reasoning behind this scope is explained in the Course Description: pairing inexpensive display hardware with real depth of programming content works because the course stays focused, rather than trying to be a full robotics course. If you want to build a complete robot with wheels and sensors around the face you design here, you'd pair this course with a separate robotics curriculum — this book is meant to be one well-taught piece, not the whole machine.

What is a MicroSim, and how many are in this textbook?

A MicroSim is a small, interactive, browser-based simulation embedded directly in a page of the textbook — you don't install anything or leave the page to use one. This book includes roughly 48 MicroSims spread across its chapters.

For example, instead of just reading a paragraph explaining how a quadrant fill code restricts an ellipse() drawing to one quarter of a shape, a MicroSim might let you drag a slider and watch a half-drawn eyelid update in real time on a simulated OLED screen. Other MicroSims let you step through the order in which drawing commands execute, or pack a color into the RGB565 format one bit at a time and watch the resulting color swatch change.

MicroSims exist because some ideas in this course — coordinate systems, timing loops, color-bit-packing — are much easier to understand by manipulating them than by reading about them. They're also a nice bridge for later chapters: many of the animation and color concepts you'll eventually implement in MicroPython on real hardware, you'll have already explored visually in a MicroSim first.

Teachers and club mentors can also embed any MicroSim in an external classroom page or LMS using a plain <iframe>. See About This Book for more on how MicroSims fit into the book's overall design.

Where should I start if I want to begin building right away?

If you want to get a face on a screen as fast as possible rather than reading background material first, start with the Getting Started page. It walks through ordering the four-part minimal kit, installing the Thonny editor, and copying a ready-made program called simple-face.py onto your Raspberry Pi Pico — most students see their first face appear on the display within a single class period.

After that first face is working, move on to Chapter 1: Hardware & Electronics Foundations, which explains the wiring and components you just used in more depth before the course moves into MicroPython programming and drawing code in the chapters that follow.

If you'd rather explore before committing to a linear path — for example, to preview what a finished capstone project looks like, or to check what a specific chapter assumes you already know — the homepage lists every section of the book, including Chapters, Lessons, Stories, the Learning Graph, and the Glossary, so you can browse before you dive in. Either path is reasonable: the course is built to reward jumping straight into hardware, but it also rewards a slower survey first.

Core Concepts

What is the SPI interface, and why do robot face displays use it?

SPI (Serial Peripheral Interface) is a fast serial communication protocol that moves data between a microcontroller and a chip using a small set of dedicated wires: a clock line, a data line, and a chip select line. The 240x240 color round display's GC9A01 Display Driver connects to the Raspberry Pi Pico over SPI because pushing a full frame of color pixel data is a lot of bytes, and SPI's separate clock and data lines let bits be read reliably at high speed. The chip select line lets one Pico talk to multiple SPI devices that share the same clock and data wires without them interfering with each other. This is different from the slower, two-wire I2C Interface this course pairs with the SSD1306-driven monochrome OLED, where far less data needs to move each frame. Knowing which interface a display uses matters before you even open an editor, since it determines which physical wires you connect and which driver library your code imports.

Example: Wiring the GC9A01 round display requires clock, data, and chip-select SPI wires plus a data/command pin and a reset pin, while the SSD1306 OLED in this course typically needs only four I2C wires total.

More on wiring both displays appears in Hardware & Electronics Foundations.

How do the 128x64 OLED and 240x240 color round display compare as design choices?

The 128x64 Monochrome OLED and the 240x240 Color Round Display are the two hardware targets this entire course is built around, and each pushes a designer toward different choices. The OLED is a rectangular, on-or-off screen: every pixel is either lit or dark, which keeps its frame buffer tiny and its redraws fast, but rules out color cues like a red angry glow or a pink blush. The round display shows full RGB565 color across a much larger 240x240 pixel grid, but that grid is visible only through a circular bezel, so a face design has to keep its features away from the square's hidden corners. The round display's larger, color frame buffer also takes longer to redraw and uses noticeably more of the Raspberry Pi Pico's limited RAM. Because both displays are driven through MicroPython's FrameBuf module, the same draw_face() logic can often run on either screen with only the driver setup and a few color-versus-monochrome details changed.

Example: A "surprised" face with wide eyes and raised eyebrows reads clearly on the OLED using only on/off pixels, while the same expression on the round display could add a warm color glow behind the eyes for extra emphasis.

Both displays are introduced together in Hardware & Electronics Foundations, and the overall course scope is summarized in the course description.

What is a screen-based robot face, and how did Anki Cozmo popularize it?

A Screen-Based Robot Face is an expression system that uses a digital screen to draw eyes, eyebrows, and a mouth instead of using physical moving parts like motorized eyelids. Because the face is just pixels on a screen, a robot can switch between completely different expressions instantly through code, with no mechanical parts to wear out or jam. Anki Cozmo, released in 2016, is one of the clearest commercial examples: it showed only two simple animated eyes on a small screen, yet reviewers and customers consistently described it as having real personality. Cozmo proved that a screen-based face didn't need a large screen, a full face outline, or realistic detail to feel expressive — a tiny pair of animated eyes was enough for people to read curiosity, frustration, or delight into the robot's behavior. This is the same design idea this course builds on: a small, low-cost screen combined with parameterized drawing code can create a face that feels alive.

Example: Cozmo's eyes could shift from wide and round to narrow and squinted using nothing more than changed pixel shapes, communicating suspicion or excitement without any moving hardware.

This history is covered in depth in A History of Screen-Based Robot Faces.

What is a social robot?

A Social Robot is a robot built primarily to interact with people through recognizable behaviors like facial expression, voice, or gesture, rather than to perform an industrial task like welding a car frame or picking items in a warehouse. Its main job is communication and companionship, not physical labor. This course's four case-study robots — Anki Cozmo, Anki Vector, Emotix Miko, and Blue Frog Robotics Buddy — are all social robots, and every one of them relies on a screen-based face as its primary way of appearing emotionally expressive to a human. A social robot doesn't need arms, legs, or even wheels to succeed at its purpose; it needs to be read by a person as having a mood, a personality, or a reaction. That's exactly why a low-cost screen showing two animated eyes can be enough hardware to build a convincing social robot, which is the design challenge at the center of this course.

Example: Anki Cozmo was marketed as a social robot meant to feel like a companion rather than a tool, and its entire emotional presence came from its small animated-eye screen and simple treads.

Social robots and the companion robot category are discussed in A History of Screen-Based Robot Faces.

What is a variable in MicroPython, and how is it used in a robot face program?

A Variable is a named storage location in a program that holds a value, such as a number or a piece of text, and can be read or changed while the program runs. In MicroPython, you create one simply by assigning it a value, like eye_size = 12. Variables are the foundation of everything this course builds afterward: instead of hard-coding a fixed drawing for every expression, a robot face program stores values like eye size, eyebrow angle, and mouth curvature in variables, then feeds those variables into drawing methods. This is different from a Constant, which is also a named value but is set once and not intended to change while the program runs, such as a screen's fixed width. Because a variable's value can be reassigned at any point, the same line of drawing code can produce a wide-eyed surprised look one moment and a narrow, sleepy look the next, just by changing what a variable holds beforehand.

Example: Setting eyebrow_angle = -20 and later changing it to eyebrow_angle = 15 lets the exact same drawing call produce a lowered, angry-looking eyebrow first and a raised, surprised-looking eyebrow second.

Variables and constants are introduced in MicroPython Fundamentals I: Syntax, Data & Loops.

How does a function definition let you reuse robot face drawing code?

A Function Definition is a block of code introduced with the def keyword that groups a set of instructions under one reusable name, optionally accepting inputs and producing an output. Instead of retyping every drawing command each time a face needs to appear, a function definition like def draw_face(mood): bundles all of that logic in one place. Once defined, calling draw_face("happy") runs the entire sequence of ellipse, rectangle, and poly calls needed to render a smiling expression, without the programmer having to remember or repeat each individual drawing step. This matters enormously as a robot face project grows: without functions, a program that supports ten expressions would need ten nearly-identical blocks of copy-pasted drawing code, and fixing a bug in the eye-drawing logic would mean fixing it in ten different places. With a function definition, that logic lives in exactly one location, so it can be tested once, fixed once, and trusted everywhere it's called.

Example: Defining def draw_eyebrow(x, y, angle): once lets a program call it four separate times — once for each side of the face — instead of writing the eyebrow-drawing code out twice for the left and right sides.

Functions are introduced in MicroPython Fundamentals II: Functions & the FrameBuf Module.

How does the Frame Buffer relate to the show() method?

A Frame Buffer is a region of memory that holds the color or on/off value of every pixel for one complete image, before that image ever reaches the physical screen. Every drawing call in this course — fill(), ellipse(), poly(), and the rest — only changes values sitting inside this in-memory frame buffer; nothing about the physical display changes yet. The Show Method, called as oled.show() or the equivalent for the color display, is the step that actually copies the frame buffer's current contents to the screen over SPI or I2C. This two-step relationship is deliberate: it lets a program draw many shapes — eyes, eyebrows, a mouth, maybe a cheek highlight — one at a time in memory, then push the finished picture to the screen in a single, quick transfer, rather than flashing each individual shape onto the display as it's drawn. Forgetting to call the show method is a common beginner mistake, since the code will run without errors but the screen will never visibly update.

Example: A program can call fill(0), then draw two eyes, an eyebrow, and a mouth entirely inside the frame buffer, and none of it appears on the OLED until a single closing oled.show() call.

The frame buffer and show method are explained in Display & Coordinate Systems.

What is a pixel, and how does display resolution affect a robot's face?

A Pixel is the smallest individually controllable point of light or color on a display, addressed by an (x, y) coordinate and set to a single color or on/off value. Display Resolution describes how many pixels a screen has in each dimension — width by height — and it directly limits how much visual detail a drawn face can show. The 128x64 OLED has 8,192 total pixels arranged in a wide rectangle, while the 240x240 round display has 57,600 pixels arranged in a square. More pixels mean finer curves, smoother-looking eyebrows, and more room to space out facial features without them overlapping. Fewer pixels force a designer to be efficient: on the low-resolution OLED, a two-pixel-wide line might be the thinnest an eyebrow can be before it becomes invisible from across a classroom, so every pixel used has to count. Resolution differences are exactly why the same draw_face() logic often needs to scale its numbers rather than use identical fixed pixel counts on both screens.

Example: A pupil drawn with a 3-pixel radius looks like a clear, sharp dot on the OLED's smaller pixel count, but might look tiny and hard to see against the round display's much higher 240x240 resolution.

Pixels and resolution are covered in Display & Coordinate Systems.

Where is the origin located in the screen coordinate system used by these displays?

The Screen Coordinate System maps every pixel on a display to a unique (x, y) position, letting drawing methods specify exactly where a shape should appear. Both displays in this course follow the same convention: Origin At Upper-Left, meaning pixel (0, 0) sits at the very top-left corner of the screen rather than the bottom-left, like a typical math graph, or the center. From that starting point, the X Axis Direction increases moving rightward, and the Y Axis Direction increases moving downward toward the bottom of the screen. This trips up a lot of beginners, because it's the opposite of the y-axis behavior in a standard math class, where up is usually positive. In practice, it means that to move a facial feature lower on the screen — like drooping an eyebrow to look sad — you increase its y-value rather than decreasing it.

Example: Placing a left eye at (30, 20) and a right eye at (90, 20) puts both eyes at the same height near the top third of a 128x64 OLED, since y=20 is closer to the top edge than the bottom.

This coordinate convention is explained in detail in Display & Coordinate Systems.

How does a drawing color value differ between the monochrome OLED and the color display?

A Drawing Color Value is the number passed to a drawing method that tells it which color or on/off state to use, and what that number means depends entirely on which display you're drawing to. On the OLED, which follows the Monochrome Color Model, there are only two valid values: 0 turns a pixel off (dark) and 1 turns it on (lit) — there's no in-between shade. On the color round display, a drawing color value is instead a full 16-bit RGB565 Color Model number that can represent thousands of distinct colors, such as 0xF800 for pure red. This difference is one of the main things that changes when porting a face design from one display to the other: a call like oled.pixel(x, y, 1) on the monochrome screen becomes something like display.pixel(x, y, 0xFFFF) for white on the color screen. Understanding this distinction early avoids a confusing bug where code that works perfectly on one display draws nothing, or the wrong color, on the other.

Example: A drawing color value of 1 lights a single pixel white on the OLED, while the equivalent bright pixel on the round display needs a full RGB565 value like 0xFFFF.

Drawing color values are introduced in Basic Drawing Primitives.

How would I use the rectangle method to draw a facial feature?

The Rectangle Method, called as rect(x, y, w, h, c), draws the outline of a rectangle starting at position (x, y) with a given width and height, using a specified drawing color. It's one of the simplest FrameBuffer drawing tools, but it's genuinely useful for robot face features that don't need a curve: a flat eyebrow, a screen border, or a blocky eye shape can all start from a rectangle. To fill the entire rectangle solid instead of just outlining it, you'd use the related Filled Rectangle Method, fill_rect(x, y, w, h, c), which colors every pixel inside the shape rather than just its edges. Choosing between the two comes down to what you're drawing: an outline works well for a border you want to layer other shapes inside of, while a filled rectangle is faster for a solid block of color like a cheek highlight or an eyelid drawn as a closed shape.

Example: Calling oled.fill_rect(20, 40, 15, 4, 1) draws a short, solid horizontal bar that can serve as a simple flat mouth for a stern or neutral expression.

The rectangle and filled rectangle methods are covered in Basic Drawing Primitives.

How would I use the blit method to reuse a pre-drawn sprite?

The Blit Method, called as blit(source, x, y, key), copies another frame buffer's pixels onto the current one at position (x, y), optionally skipping a transparent color so the background underneath still shows through. This is powerful for a robot face because it lets you draw a complicated shape once, store it as a small pre-built Sprite, and then stamp copies of it onto the screen repeatedly without redrawing every individual pixel or curve each time. That's both less code to write and, since blitting copies existing pixel data rather than recalculating a shape, noticeably faster than redrawing something like a detailed pupil from scratch every frame. To make the sprite's background invisible against whatever it's placed over, you pass a Transparent Color Key — a color value the blit method will skip copying, so only the meaningful pixels of the sprite are transferred.

Example: A small heart-shaped sprite can be pre-drawn once, then blitted directly over an eye using a transparent color key so only the heart shape appears, not a black square around it, showing a "delighted" reaction.

The blit method and sprites are explained in Basic Drawing Primitives.

How would I use the ellipse method to draw a robot's eyes?

The Ellipse Method, called as ellipse(x, y, xr, yr, c, f, m), draws an oval centered at (x, y) with a horizontal radius xr and a vertical radius yr, optionally filled solid and optionally limited to certain quadrants. To draw a round eye, you'd call it with equal xr and yr values — since a circle is just a special case of an ellipse where both radii match — and set the fill flag to True for a solid pupil or False for just an outline. Because xr and yr can be set independently, the same method can also stretch an eye into a tall oval for a surprised look or a wide, squashed oval for a squint, just by changing those two numbers. This flexibility is why the ellipse method sits at the center of so much of this course's facial drawing code: eyes, pupils, and curved eyebrows and mouths all rely on it.

Example: Calling oled.ellipse(30, 20, 8, 8, 1, True) draws a solid, round eye 8 pixels wide and 8 pixels tall centered at (30, 20).

The ellipse method and its parameters are covered in Ellipse & Polygon Drawing.

What is the difference between the Ellipse Method and the Poly Method?

The Ellipse Method and the Poly Method are the two curve-and-shape-drawing tools introduced together in MicroPython's FrameBuf module, but they solve different drawing problems. The ellipse method draws ovals and circles from just a center point and two radius values, making it ideal for naturally rounded features like eyes, pupils, and softly curved mouths — it even supports a Quadrant Fill Code that limits the fill to specific quarters of the oval, useful for a bottom-half smiling mouth curve. The poly method, by contrast, draws or fills a polygon defined by a list of vertex coordinates called a Point Array, connecting each point in order and automatically closing the shape back to its starting vertex. This makes poly the right tool for angular, straight-edged shapes an ellipse simply cannot produce, like a sharp triangular eyebrow for an angry expression. In short: reach for ellipse when a feature should look rounded, and reach for poly when a feature needs distinct corners.

Example: A calm, curved eyebrow works well as a partial ellipse, while an angry, pointed eyebrow is usually built with poly() from just three vertices forming a triangle.

Both methods are compared in detail in Ellipse & Polygon Drawing.

When did ellipse() and poly() become part of official MicroPython?

The ellipse() and poly() FrameBuffer drawing methods first became part of an official, stable MicroPython release with the MicroPython V1.20.0 Release in April 2023. Before that date, these methods existed only in MicroPython's development branch or in unofficial nightly builds, meaning any hobbyist who wanted curved or angular drawing methods had to either build custom firmware themselves or fall back on manual workarounds like drawing many short line segments to approximate a curve. This is a fairly recent addition in the history of MicroPython, which is worth knowing because it explains why many older MicroPython tutorials and forum posts online don't use ellipse() or poly() at all — those methods simply didn't exist yet when that older content was written. This course assumes v1.20.0 or later firmware is installed on the Raspberry Pi Pico, since so much of the facial drawing code depends on these two methods being available out of the box.

Example: A student following an older online MicroPython tutorial from 2021 would find no mention of ellipse() at all, since it wasn't added to a stable release until nearly two years later.

The full version history is traced in A History of MicroPython's FrameBuf Drawing Support.

What is the face outline, and does every robot face need one drawn explicitly?

The Face Outline is the overall boundary shape framing a robot's drawn face, and whether it needs to be explicitly drawn depends on which display you're using. On the 128x64 OLED, a rectangular screen, a designer might choose to draw a border or leave the face features floating with no outline at all — it's optional and a matter of style. On the 240x240 round display, though, the physical circular bezel already acts as a natural face outline, so many designs skip drawing an extra border entirely and simply let the screen's own round edge frame the face. This decision affects how much visual "real estate" is available for features: a drawn outline eats into the space eyes, eyebrows, and a mouth can use, while relying on the screen's physical edge frees up that space. Either way, the face outline is usually the first thing considered in a Face Layout Grid, since every other feature gets positioned relative to it.

Example: A round-display face design might draw nothing but eyes, eyebrows, and a mouth, letting the physical circular screen edge itself read as the face's outline, the way a smartwatch face uses its own bezel.

Face outline and layout planning are covered in Facial Anatomy & Layout Design.

How does eyebrow shape communicate a robot's emotional state?

Eyebrow Shape is the overall form used to draw an eyebrow — a straight line, a curved ellipse arc, or an angular polygon — and it's widely considered the single most powerful facial feature for reading a robot's mood, often even more important than the mouth. A calm, gently curved eyebrow shape reads as relaxed or content, while a sharp, angular triangle eyebrow shape angled steeply toward the nose reads immediately as angry, even if nothing else about the face changes. Combined with the Eyebrow Angle Parameter, which controls how much the shape tilts, a designer can move a face from surprised (eyebrows raised high) to stern (eyebrows level but lowered) to angry (eyebrows sharply angled inward) using the same underlying drawing code with different numbers plugged in. Because eyebrows carry so much emotional weight, expression design often starts by deciding what the eyebrows should look like before finalizing the eyes or mouth.

Example: Keeping the mouth shape identical but changing only the eyebrow shape from a flat line to a steep, inward-pointing triangle can shift a face's read from "neutral" to clearly "angry."

Eyebrow shape and its role in expression are discussed in Facial Anatomy & Layout Design.

How would I change mouth shape to turn a neutral face into a happy one?

Mouth Shape is the overall form used to draw a robot's mouth, ranging from a simple straight line to a curved arc or polygon, and it's one of the most direct ways to shift an expression's emotional read. A neutral expression typically uses a flat mouth shape, often just a horizontal line or a barely-curved arc. To turn that into a happy expression, you'd increase the Mouth Curvature Parameter and switch from a flat line to a Bottom-Half Mouth Curve — using the ellipse method's quadrant fill code to draw only the lower two quarters of an oval, which produces a clean upward-curving smile shape. The wider and deeper that bottom-half curve, the bigger and more obvious the smile reads, which is also how Expression Intensity gets built into the same underlying drawing logic.

Example: Changing a flat line() mouth into an ellipse() call filled only on its bottom two quadrants, with a wider radius than the neutral version, converts a flat neutral mouth into a clear, wide smile using the same drawing method family.

Mouth shape and curvature are covered in Facial Anatomy & Layout Design.

How would I decide where to place the eyes and pupils on a robot face?

Eye Placement refers to the chosen x and y coordinates for each eye, and it strongly affects how symmetrical, alert, or sleepy a face reads before a single eyebrow or mouth is even drawn. As a starting point, most neutral designs place both eyes at the same height for Facial Symmetry, spaced apart by an amount that scales with the screen's width — closer together on the narrow OLED, farther apart on the wider round display. Once the eyes themselves are positioned, the Pupil — the smaller dark circle drawn inside each eye representing where the robot appears to be looking — gets placed relative to the eye's center, usually starting there for a straight-ahead gaze. Shifting eye placement slightly higher on the screen can make a face look more alert or surprised, while shifting pupils toward one edge of their eyes makes the robot appear to glance sideways.

Example: Placing both eyes at y=20 with the left eye at x=30 and the right at x=90 creates a symmetrical, forward-looking neutral face on a 128-pixel-wide OLED; nudging both pupils toward the left edge of each eye afterward makes the robot appear to glance left.

Eye and pupil placement are discussed in Facial Anatomy & Layout Design.

What is parameterized face design, and why is it central to this course?

Parameterized Face Design is the approach of controlling feature sizes, positions, and angles with variables rather than hard-coded numbers, so that one set of drawing code can produce many different expressions. Instead of writing separate, nearly-identical drawing code for a happy face, a sad face, and a surprised face, a parameterized design stores values like eye size, eyebrow angle, and mouth curvature in a small set of numbers that get fed into the same drawing calls. Change the numbers, and the same code draws a different expression. This is the single biggest idea the whole course builds toward: it's what makes a draw_face() function scale to a dozen expressions without a dozen copies of similar code, and it directly reflects the computational thinking skill of Abstraction — treating an emotion as a handful of numbers rather than a fixed picture.

Example: Instead of separately hard-coding a "surprised eyebrow" shape and an "angry eyebrow" shape, a parameterized design uses one eyebrow-drawing function that takes an angle value, producing a raised eyebrow for one number and a lowered, sharp one for another.

Parameterized face design is introduced in Facial Anatomy & Layout Design.

How does the draw_face() function put parameterized face design into practice?

The Draw Face Function, typically written as draw_face(), is the central reusable function in this course's projects: it accepts a mood or a set of feature parameters and calls every drawing method needed to render one complete expression. It's the direct, practical implementation of Parameterized Face Design — rather than that design principle staying an abstract idea, draw_face() is where it actually happens in code. Calling draw_face("happy") might look up a stored set of values for eyebrow angle, eye size, and mouth curvature, then pass those values into calls to ellipse(), poly(), and rect() to build the finished picture, followed by a call to show() to display it. Because the function accepts parameters instead of containing fixed drawing steps for each mood, adding a brand-new fourteenth expression means adding one new set of parameter values, not writing a new function from scratch.

Example: A single draw_face(mood) function can produce both a neutral face and an excited face just by looking up different eyebrow angle, eye size, and mouth curvature values for each mood name before running the same drawing calls.

The draw face function is covered in detail in Facial Anatomy & Layout Design.

What is the neutral expression, and why does every face design start there?

The Neutral Expression is a baseline facial expression showing no strong emotion, typically drawn with level eyebrows, evenly open eyes, and a flat mouth. It's the expression a robot face returns to by default whenever no button has been pressed recently and no other emotion is currently being signaled. Neutral matters far beyond being just one expression among many: it's usually stored as the Default Face Parameters, the baseline set of values that every other expression is built by adjusting. Raise the eyebrows from neutral and you get surprised; lower and angle them sharply and you get angry; curve the mouth upward and you get happy. Designing a good neutral expression first, before any of the more dramatic ones, makes every other expression easier to build and easier to compare, since each one is really just "neutral, plus a specific set of changes."

Example: A robot face program left idle for ten seconds with no button presses might automatically fall back to drawing its neutral expression, keeping level eyebrows and a flat mouth until the next input arrives.

The neutral expression and the full core expression set are covered in Emotion Theory & the Core Expression Set.

How does expression intensity let one drawing function create both a small smile and a huge grin?

Expression Intensity is a parameter that scales how strongly an expression's features deviate from the neutral expression, letting a mild version and an extreme version of the same emotion share identical underlying drawing logic. Rather than writing separate code for "a little bit happy" and "extremely happy," a design can multiply the difference between neutral and happy parameter values by an intensity number — a low intensity nudges the mouth curvature parameter only slightly past neutral, while a high intensity pushes it much further. This keeps the drawing code simple while still giving a robot face a much wider emotional range than a fixed set of expressions could offer on its own, and it connects directly to the Valence Arousal Model, which treats emotional strength as something that can vary along a continuous scale rather than switching on and off.

Example: Setting happy's expression intensity to 0.3 might produce a small, closed-lip smile, while setting it to 1.0 on the same underlying formula produces a wide, open grin using the same mouth-drawing code.

Expression intensity is discussed in Expression Design, Readability & Human-Robot Interaction.

The Animation Loop is the repeating block of code, usually inside a while loop, that continuously updates and redraws a robot face over time to create motion. To make a face blink, the loop needs to check on every pass whether it's time to blink, based on how much time has elapsed since the last blink — this is where Ticks Function Timing and Ticks Diff Calculation come in, reading MicroPython's millisecond counter and comparing it against a stored "last blink" time. When enough time has passed, the loop briefly redraws the eyes as closed eyelid shapes for a couple of frames, then switches back to open eyes and resets the timer. Doing this without pausing the whole program (avoiding a blocking sleep() call) is what lets the loop keep checking for button presses at the same time, which is the idea behind the Non-Blocking Delay Pattern.

Example: An animation loop might check every pass whether 3000 milliseconds have passed since the last blink; if so, it draws closed eyelids for two frames, calls show(), waits briefly, then redraws open eyes and resets the blink timer.

Building an animation loop is covered step by step in Animating Expressions: Timing & Motion.

How would a mode state machine control what a button press does on a robot face?

A Mode State Machine is a program structure that tracks which operating mode a robot face is currently in — such as an idle mode, an expression-select mode, or a parameter-tuning mode — and changes what a button press means depending on that current mode. Rather than a single button always doing the same thing, a mode state machine stores the current mode in a Global State Variable and checks it before deciding how to respond to input: in idle mode, pressing the button might enter the expression menu, while in menu mode, the same button might confirm a selection instead. This lets a small number of physical controls — often just one or two buttons and a knob — do far more than they could if each control only ever performed one fixed action.

Example: A single button might cycle to the next expression when the robot is in "menu mode," but return the robot to its default idle state when held down, with a mode state machine determining which behavior applies based on the current mode variable.

Mode state machines are introduced in Interactive Controls: Inputs & Concurrency.

How does the RGB565 color model pack color into a single number?

The RGB565 Color Model is a 16-bit color format that packs three separate color intensities — red, green, and blue — into one number: 5 bits for red, 6 bits for green, and 5 bits for blue. Green gets one extra bit because human eyes are more sensitive to shades of green than to red or blue, so that extra bit of precision is where it's most noticeable. This packing is done using Bit Shifting, moving each color's bits into its correct position within the final 16-bit number before combining them, usually through a helper Color565 Function that takes plain red, green, and blue values and returns the packed result. RGB565 is what makes the color round display capable of showing thousands of distinct colors, in contrast to the OLED's Monochrome Color Model, where a pixel can only ever be fully on or fully off.

Example: Calling a color565() function with red=255, green=100, blue=50 returns a single 16-bit number representing that specific orange color, ready to pass directly into a drawing method like fill_rect().

RGB565 and the color565() function are explained in Porting Faces to a Color Display.

What is computational thinking, and how does it apply to designing a robot face?

Computational Thinking is a problem-solving approach that breaks challenges into steps a computer can carry out, using techniques like Abstraction, Decomposition, Modularity, and Pattern Recognition. It's the underlying skill set this entire course is quietly teaching alongside MicroPython and display hardware. Turning a vague goal like "make the robot look happy" into a specific, working draw_face("happy") function call requires all four of these techniques at once: abstraction reduces the emotion "happy" down to a handful of numeric parameters, decomposition splits the face into separately drawable eyes, eyebrows, and a mouth, modularity keeps that logic reusable in one function rather than scattered everywhere, and pattern recognition notices that several expressions can share the same underlying drawing code with only a few values changed. These are the same habits professional programmers use on projects far larger than a robot face, which is why this course treats them as a core learning goal, not just a side effect of writing code.

Example: Recognizing that happy, excited, and surprised expressions all reuse the same mouth-drawing logic with only the curvature value changed is pattern recognition — one piece of computational thinking — in action.

Computational thinking is the focus of Computational Thinking & Capstone Design.

How would you break down a target expression into specific feature changes for the capstone project?

Breaking a target emotion into its component feature changes is one of the core skills the Capstone Project asks students to demonstrate. Rather than guessing at a whole new drawing from scratch, the process starts from the Neutral Expression and asks, one feature at a time, what has to change: does Eyebrow Shape need to angle sharply or stay level? Does eye size need to grow or shrink? Does Mouth Shape need to curve up, down, or stay flat? For a target like "surprised," that analysis lands on wide, enlarged eyes, eyebrows raised higher than any other expression, and a small open circular mouth — while "stern" instead keeps the mouth flat and lowers the eyebrows only slightly, without the sharp inward angle anger would use. This same decomposition process is exactly what a good Expression Set Planning stage looks like before any drawing code gets written for a capstone project, and it's what lets a student justify, feature by feature, why their design reads clearly as the emotion they intended.

Example: Planning a "confused" expression might specifically call for one eyebrow raised and the other left at its neutral angle, breaking facial symmetry on purpose, while both eyes and the mouth stay unchanged from neutral.

Expression planning for the capstone is covered in Computational Thinking & Capstone Design.

What is a Data Type, and how do Integer and Boolean types differ?

A Data Type is a classification that tells a MicroPython program what kind of value a variable holds — a whole number, a true/false value, or text — and that classification determines which operations are valid on it. The Integer Data Type represents whole numbers with no decimal point, and it is required for anything that addresses a pixel, since a display has no such thing as half a pixel: the x and y arguments passed to pixel() must always be integers. The Boolean Data Type holds only one of two values, True or False, and is commonly used as a flag, such as whether a shape should be drawn filled or outlined. Recognizing which data type a variable holds is one of the earliest skills a student needs before writing any drawing code, since MicroPython raises an error the moment a value is used the wrong way.

Example: The fill parameter of the rect() method uses the Boolean Data Type to decide between an outline and a solid rectangle, while the x and y coordinates passed alongside it must use the Integer Data Type.

Variables, constants, and these core data types are introduced in MicroPython Fundamentals I.

What is the MicroPython REPL, and how does Thonny IDE make it accessible?

The MicroPython REPL is an interactive prompt built into MicroPython that lets a programmer type a single line of code and immediately see its result, without saving a whole program first — the name stands for Read-Evaluate-Print-Loop. This makes it a fast way to test a small idea, such as one drawing call, before adding it to a full script. The Thonny IDE is the beginner-friendly code editor this course uses to reach that REPL: it connects to a Raspberry Pi Pico over USB, provides a REPL panel at the bottom of the window, and lets a student upload and run complete MicroPython programs with a single click. Without Thonny IDE, a student would need a separate serial terminal program to reach the same REPL. Together, these tools form the everyday workflow of this course: quick experiments in the REPL, followed by saving working code as a script.

Example: Typing oled.fill(0) at the MicroPython REPL inside Thonny IDE clears the display instantly, letting a student test drawing commands one at a time.

See MicroPython Fundamentals I for a full walkthrough of getting connected.

How does a Conditional Statement work with a Function Return Value?

A Conditional Statement, written with if, elif, and else, runs different blocks of code depending on whether a condition is true or false. A Function Return Value is the result a function sends back to the code that called it, using the return keyword, which the caller can then store in a variable or use immediately. These two concepts often work together: a function may use a conditional statement internally to decide which number to compute, then hand that number back as its function return value. Without a conditional statement, a function could only ever produce one fixed answer no matter what was passed in; without a function return value, the result of that decision would have nowhere to go once computed.

Example: A helper function using if mood == "happy": return 20 and else: return 0 relies on a conditional statement to decide exactly what its function return value will be.

Both concepts are introduced together in MicroPython Fundamentals II.

What happens during Display Initialization, and what does the Display Reset Pin do?

Display Initialization is the one-time setup sequence, run near the start of a program, that configures a display driver's settings and prepares it to receive frame buffer data. Before this sequence runs, a display cannot reliably show anything, even if it is correctly wired and powered. One step commonly included in display initialization is toggling the Display Reset Pin, a control line that, when pulsed low for a moment, restarts the display driver chip to a known starting state. Skipping display initialization, or wiring the display reset pin incorrectly, is one of the most common reasons a brand-new display shows nothing, or shows garbled pixels, the first time a student powers it on. Because it only needs to run once per program, display initialization is normally placed right after importing the driver module and before the main animation loop.

Example: The initialization code for the GC9A01 color display briefly pulls the Display Reset Pin low before sending any configuration commands, ensuring the chip always starts from the same clean state.

See Display & Coordinate Systems for the full sequence used with both displays.

How does a Low-Cost Robotics Kit involve a Hardware Cost Trade-Off?

A Low-Cost Robotics Kit is an affordable bundle of a microcontroller, display, and basic components assembled for classroom or hobbyist use, prioritizing accessibility over the polish of a commercial robot product. This course is built around exactly that: a Raspberry Pi Pico, buttons, a potentiometer, and either a monochrome OLED or a color round display. Every low-cost robotics kit involves making a Hardware Cost Trade-Off — weighing a more capable or expensive component against a cheaper option that limits some feature. A classroom outfitting many students with a low-cost robotics kit might accept several such trade-offs at once, such as a cheaper microcontroller board or skipping a rotary encoder, to keep every kit affordable.

Example: Choosing the 128x64 monochrome OLED over the 240x240 color round display is a Hardware Cost Trade-Off: it saves money and memory but removes color-based emotion cues like a red angry glow.

Hardware & Electronics Foundations walks through pricing out a complete kit.

How does Draw Order Layering affect the Line Method and Pixel Method?

The Line Method, called as line(x1, y1, x2, y2, c), draws a straight line between two arbitrary points at any angle, useful for diagonal features like a sharply angled eyebrow. The Pixel Method, called as pixel(x, y, c), sets a single pixel to a given color, useful for placing one bright dot to represent a glint of light in an eye. Both are affected by Draw Order Layering, the principle that shapes drawn later in a program overwrite pixels from shapes drawn earlier at the same position, so the order of drawing calls determines the final visible image. A glint added with the Pixel Method must be drawn after the eye shape beneath it, not before, or Draw Order Layering would immediately bury it.

Example: Calling fill_rect() for an eye and then pixel() for its glint relies entirely on Draw Order Layering to keep the glint visible on top.

See Basic Drawing Primitives for more on layering drawing calls correctly.

What is the FrameBuf Version Timeline, and how is it tracked?

The FrameBuf Version Timeline is the sequence of MicroPython releases across which the framebuf module gained new drawing methods and bug fixes, from its original basic method set through the addition of ellipse() and poly() and later refinements. This history is tracked using MicroPython Release Versioning — the major.minor.patch numbering scheme, such as 1.20.0, that distinguishes feature releases from smaller bug-fix updates. Anyone curious how a specific point on the FrameBuf Version Timeline came about can trace it through the MicroPython GitHub Repository, the public code repository hosting MicroPython's source code, issue tracker, and pull request history.

Example: Searching the MicroPython GitHub Repository turns up the original pull request proposing the ellipse-drawing code, long before it appeared in a numbered stable release under MicroPython Release Versioning.

See A History of MicroPython's FrameBuf Drawing Support for the complete timeline.

How can Eyelid Representation and the Eye Size Parameter combine in an expression?

An Eyelid Representation is a facial feature drawn as a filled rectangle or shape over part of an eye, used to show blinking, sleepiness, or a lowered gaze. The Eye Size Parameter is a separate variable that controls the radius or dimensions used to draw each eye itself, and it can be widened for surprise or narrowed for suspicion, independent of any eyelid. A surprised expression might enlarge the Eye Size Parameter while keeping the Eyelid Representation fully retracted, showing as much of the eye as possible, while a sleepy expression keeps the Eye Size Parameter normal but draws the Eyelid Representation covering roughly half of each eye.

Example: Animating a slow blink means gradually increasing the height of the Eyelid Representation across several frames until the eye briefly disappears, without ever changing the underlying Eye Size Parameter.

See Facial Anatomy & Layout Design for how both parameters fit into a complete eye design.

What are the Ekman Universal Emotions, and how does Basic Emotion Theory support them?

Basic Emotion Theory is the psychological theory, associated with Paul Ekman, that a small number of emotions are biologically universal and expressed through recognizable, consistent facial patterns regardless of culture. The specific set this theory identifies are the Ekman Universal Emotions — commonly happiness, sadness, anger, fear, surprise, and disgust — found to be recognized through facial expression across many different cultures. This course's core expression set is built directly from the Ekman Universal Emotions, giving each robot face expression a scientifically grounded design target rather than an arbitrary one.

Example: A robot face project that includes happy, sad, angry, afraid, surprised, and disgusted expressions is directly implementing the Ekman Universal Emotions that Basic Emotion Theory identifies as most reliably recognized.

See Emotion Theory & the Core Expression Set for the full expression set and the theory behind it.

How does Minimal Feature Robot Research explain the Cozmo Emotion Engine's success?

Minimal Feature Robot Research studies how few facial elements — sometimes just two animated eyes — a robot needs before people stop recognizing it as expressive. This research helps explain why Anki Cozmo succeeded with such simple hardware: its tiny screen showed only two animated eyes, yet reviewers consistently described it as having real personality. The software behind that illusion was the Cozmo Emotion Engine, the internal system that selected and blended animated behaviors and screen expressions based on the robot's simulated internal state. Minimal Feature Robot Research suggests viewers don't need a full face to project emotion onto a robot — a small number of well-timed feature changes is enough, exactly as the Cozmo Emotion Engine demonstrated commercially.

Example: The Cozmo Emotion Engine could make the robot appear "frustrated" after repeatedly failing to stack a cube, using nothing more than eye shape changes that Minimal Feature Robot Research shows are sufficient to read as emotion.

See A History of Screen-Based Robot Faces.

How does a Timing Loop enable both Blinking Animation and Expression Interpolation?

A Timing Loop is a section of code responsible for controlling how often an action, like a redraw or blink, happens relative to real elapsed time rather than just how many times the main loop has run. A Blinking Animation is a short sequence that closes and reopens the eyes over a few frames, relying on the timing loop to know when enough time has passed since the last blink. Expression Interpolation is a technique that calculates intermediate feature values between two expressions' parameters, producing a smooth transition instead of an instant jump. Both the Blinking Animation and Expression Interpolation depend on the same underlying Timing Loop to decide exactly when each intermediate frame should be drawn.

Example: Interpolating between neutral and happy might gradually increase the mouth curvature parameter over ten frames instead of switching it instantly, all timed by the same Timing Loop that also schedules the next Blinking Animation.

See Animating Expressions: Timing & Motion for how these fit into a non-blocking animation loop.

How does Potentiometer Value Mapping fit into a project's Control Mapping Design?

Potentiometer Value Mapping is the process of converting a potentiometer's raw analog reading into a useful range for a program, such as mapping a 0-65535 reading down to an eyebrow angle of -30 to 30 degrees. This mapping only becomes useful once it fits into a project's Control Mapping Design — the overall plan for which physical control, such as a button, potentiometer, or rotary encoder, performs which function. As part of control mapping design, a student decides whether the potentiometer controls eyebrow angle, mouth curvature, or something else, and Potentiometer Value Mapping is the technique that turns that decision into working code.

Example: A Control Mapping Design might dedicate the potentiometer to live eyebrow tuning while a button cycles expressions; Potentiometer Value Mapping then converts the knob's raw voltage into exactly the angle range the drawing function expects.

See Interactive Controls: Inputs & Concurrency and Building an Expression Menu & Live Controls.

What do Color Theory Basics teach about choosing a Warm Versus Cool Color?

Color Theory Basics covers foundational principles about how colors relate to and combine with each other, including ideas like primary colors and complementary pairs, applied here to expressive robot face design on the color round display. One practical application is the Warm Versus Cool Color classification: colors like red and orange are considered warm, while colors like blue and teal are considered cool, and each carries different emotional associations a designer can use intentionally. A designer applying Color Theory Basics might choose a Warm Versus Cool Color contrast deliberately, using orange for an excited expression's glow and blue for a calm one, to reinforce what the eyebrows and mouth already communicate.

Example: A student who understood Color Theory Basics chose an orange glow that stood out clearly against a teal background, using the Warm Versus Cool Color contrast to make an excited expression visually pop.

See Porting Faces to a Color Display.

What is the difference between a Design Critique and a Peer Design Review?

A Design Critique is a structured review process where a robot face design is evaluated against clear goals, such as readability and emotional clarity, to identify specific improvements. It focuses on giving and receiving feedback well, pointing out exactly what works and why, rather than vague praise. A Peer Design Review is closely related but specifically describes a feedback session in which classmates, rather than an instructor, examine and comment on each other's designs before a final version is built. In practice, a Peer Design Review is often where a Design Critique actually happens in this course, as students trade capstone sketches and apply critique skills to each other's work.

Example: During a Peer Design Review, a classmate might raise a specific Design Critique that a mouth is drawn too close to the eyes to read clearly from across a classroom.

See Computational Thinking & Capstone Design.

How do Robot Startup Funding and discontinuation shape a Robot Commercial Outcome?

A Robot Commercial Outcome is the eventual market result of a robot product, ranging from sustained sales success to discontinuation, bankruptcy, or acquisition. Robot Startup Funding is money raised by a robotics company, often through venture capital or a crowdfunding campaign, to develop and manufacture a product before it generates enough sales revenue on its own. Robot Product Discontinuation is the point at which a company stops manufacturing, selling, or supporting a product, often leaving existing units without further updates — one of the most common types of Robot Commercial Outcome for ambitious consumer robots. A company that raises generous Robot Startup Funding but cannot turn early sales into ongoing revenue often ends up facing Robot Product Discontinuation once that funding runs out.

Example: A well-funded robot startup might ship an impressive first product, only to reach Robot Product Discontinuation a few years later once its Robot Startup Funding is exhausted and sales don't cover ongoing support costs.

See A History of Screen-Based Robot Faces for real case studies.

How does the Miko Educational Robot Design differ from a Mass-Market Robot Toy?

The Miko Educational Robot Design is the product design approach behind Emotix Miko, combining a screen face with curated lessons and conversational interaction aimed at children's learning outcomes. This stands in contrast to a Mass-Market Robot Toy, a robot manufactured and priced for a broad consumer audience rather than researchers or educators, typically sold through general retail toy channels with entertainment as the primary goal. Anki Cozmo is a clearer example of a Mass-Market Robot Toy, built around personality-driven play rather than lesson content. The Miko Educational Robot Design instead treats the screen face as a tool for guiding structured learning activities, so content pacing and educational goals matter as much as expressiveness.

Example: The Miko Educational Robot Design pairs a friendly screen expression with a math quiz to keep a young user engaged, unlike a purely entertainment-focused Mass-Market Robot Toy.

See A History of Screen-Based Robot Faces.

Technical Detail Questions

What is the difference between the SPI interface and the I2C interface?

The I2C interface and the SPI interface are the two serial communication protocols this course uses to connect a display to the Raspberry Pi Pico, and they differ mainly in wiring and speed. I2C is a two-wire protocol: one shared clock line and one shared data line carry all communication, and several devices can share those same two wires because each has its own address. The 128x64 monochrome OLED, built around the SSD1306 chip, typically connects over I2C with only four total wires (clock, data, power, and ground). SPI instead uses a separate clock line and data line plus at least one chip select pin, so the microcontroller can address several devices individually rather than sharing one bus by address. Because SPI dedicates a full line to timing and does not need to negotiate addresses, it can move data faster than I2C. That extra speed matters for the 240x240 color round display, driven by the GC9A01 chip, since it must push far more RGB565 pixel data over the wire every time the screen refreshes. In short, I2C favors simple wiring with fewer pins, while SPI trades a couple of extra wires for the higher data-transfer speed a color display needs. See Hardware & Electronics Foundations for the full wiring comparison.

What does the chip select pin do on an SPI bus?

The chip select pin is an SPI control line that tells one specific device on a shared bus to pay attention to the data currently being sent, which is what allows a single microcontroller to talk to multiple SPI devices without them interfering with each other. Because SPI devices share the same clock and data lines, every device would otherwise "hear" every transmission; the chip select pin solves this by acting like a spotlight. A microcontroller pulls a target device's chip select pin low (active) right before sending it data, and keeps every other connected device's chip select pin high (inactive) so those devices ignore the transmission entirely. For example, pulling the color round display's chip select pin low tells the GC9A01 driver chip that the following bits of clock and data traffic are meant for it, not for some other SPI peripheral that might be wired to the same Pico. Without a correctly wired chip select pin, a display might never receive its setup commands, or might receive data intended for a different device. Learn more about SPI wiring in Hardware & Electronics Foundations.

Why does a button circuit need a pull-up resistor?

A pull-up resistor is a resistor connected between a signal line and a positive voltage that holds an input pin's reading high (electrically "on") until something actively pulls it low, and buttons need one because a disconnected pin has no defined value at all — it can drift and register random presses due to electrical noise. Rather than wiring an external resistor onto a breadboard for every button, most microcontrollers, including the RP2040 on the Raspberry Pi Pico, include an internal pull-up resistor that a program can enable in software. With the internal pull-up turned on, an unpressed button pin reads a steady high value; pressing the button connects the pin to ground, which pulls the reading low. This gives clean, predictable digital input reading: high means "not pressed," low means "pressed." Skipping the pull-up resistor entirely can leave the pin "floating," meaning its reading bounces unpredictably between high and low even when no one is touching the button, which would make a mode-switching button in a robot face project fire randomly. See Hardware & Electronics Foundations for wiring details.

What is the difference between the SSD1306 and GC9A01 display driver chips?

The SSD1306 display driver and the GC9A01 display driver are the two controller chips used in this course, and each is built for a different kind of screen. The SSD1306 manages the 128x64 monochrome OLED, translating FrameBuffer commands into signals that light individual pixels on or off, and it commonly communicates over the I2C interface (though SPI versions exist). The GC9A01 manages the 240x240 color round display, receiving full RGB565 pixel data over the faster SPI interface and refreshing a circular viewing area cut from a square pixel grid. Beyond the physical screens they control, the two chips also differ in setup complexity: the GC9A01's color display init sequence involves more configuration bytes and precise timing than the simpler SSD1306 startup routine, because it must configure color mode and orientation in addition to basic display power. In MicroPython code, this difference shows up mainly in the driver setup step — a program creates an ssd1306 driver object for the OLED or a gc9a01 driver object for the round display — while the drawing code built on the FrameBuf module can often stay nearly identical between the two. Details on both chips appear in Hardware & Electronics Foundations.

What is the difference between a List, a Tuple, and a Dictionary in MicroPython?

These three MicroPython collection types all group multiple values under one variable name, but they differ in whether they can change and how their contents are accessed. A List Data Structure, written with square brackets like ["happy", "sad", "angry"], is ordered and changeable — items can be added, removed, or reassigned after creation, making it a good fit for a sequence of expressions a for loop steps through during a demo. A Tuple Data Structure, written with parentheses like (40, 20), is also ordered but unchangeable once created, which makes it a natural choice for a fixed coordinate pair that the drawing code should never accidentally modify. A Dictionary Data Structure stores values under named keys instead of numeric positions, written like {"happy": 10, "sad": -10}, letting a program look up a value by a descriptive label such as a mood name rather than by its position in a sequence. Choosing between them comes down to intent: use a list for an ordered, editable sequence, a tuple for a fixed pair or group of values that shouldn't change, and a dictionary when values need to be looked up by name. See MicroPython Fundamentals I for full examples of each.

What is the difference between a For Loop and a While Loop?

A For Loop repeats a block of code a set number of times or once for each item in a sequence, such as stepping through a list of five expressions and drawing each one for two seconds during a demo — the loop naturally ends once every item has been visited. A While Loop instead repeats a block of code for as long as a given condition stays true, which makes it the right structure for a robot face's main animation loop, since that loop needs to keep blinking and checking buttons indefinitely, with no fixed number of repetitions known in advance. A simple way to remember the distinction: reach for a for loop when you know in advance how many times something should repeat, or when you're processing every item in a known collection; reach for a while loop when repetition should continue until some condition changes, such as while True: running forever until the Pico loses power. Both are fundamental control structures in MicroPython and appear constantly throughout this course's drawing and animation code. See MicroPython Fundamentals I for syntax examples of each.

How does a default parameter value change the way a function can be called?

A Default Parameter Value is a value automatically assigned to a Function Parameter when the code calling the function doesn't supply one, which lets that function be called with fewer arguments than it technically accepts. Consider def draw_face(mood="neutral"): — the parameter mood has a default value of "neutral", so calling draw_face() with no argument at all still draws a neutral expression, while calling draw_face("happy") overrides the default and draws a happy expression instead. This is useful anywhere a function has one obvious, sensible starting behavior but should still allow customization: a beginner testing the drawing code can call the function with zero arguments and get a reasonable result, while more advanced code can pass whatever mood it needs. Without a default parameter value, every single call to draw_face() would require explicitly typing a mood argument, even in the common case where "neutral" is exactly what's wanted. Default parameter values are defined once, in the function definition itself, using an equals sign after the parameter name. See MicroPython Fundamentals II for more on function parameters.

What is the difference between global and local scope for a variable in MicroPython?

Global Versus Local Scope describes where in a program a variable can be seen and used. A variable defined outside any function has global scope, meaning it's visible everywhere in the program, including inside every function — a Global State Variable like current_mood is a common example, since both a robot face's animation loop and its button-handling code need to read and agree on the same mood value. A variable defined inside a function, by contrast, has local scope: it only exists while that function is running and disappears the moment the function finishes. For instance, a blink_timer variable created inside draw_face() is local — no code outside that function can see or use it, and a fresh copy is created every time the function runs again. This distinction matters because it prevents different parts of a program from accidentally overwriting each other's temporary values, while still allowing genuinely shared information, like the current mood, to be read from anywhere. A common mistake is trying to modify a global variable from inside a function without declaring it global first, which instead silently creates a new local variable with the same name. See MicroPython Fundamentals II for a closer look at scope rules.

How do you calculate the frame buffer size needed for the 128x64 monochrome OLED?

A Frame Buffer Size Calculation multiplies a display's width by its height, then adjusts for how many bits each pixel needs, since that determines how much microcontroller RAM must be reserved as Display Buffer Memory. For the 128x64 monochrome OLED, each pixel needs only 1 bit (on or off), and since there are 8 bits in a byte, the calculation is: 128 pixels wide times 64 pixels tall, divided by 8 bits per byte, which equals exactly 1,024 bytes. This is why every screen refresh cycle on the OLED sends 1,024 bytes over I2C to update the whole display at once — that's the entire size of its frame buffer. The division by 8 reflects Byte Alignment In Buffer: eight vertical or horizontal monochrome pixels get packed together into each single byte of memory rather than each pixel getting its own byte. This calculation becomes important when planning a project's memory budget, since the RP2040 has a fixed, limited amount of RAM, and a display with more pixels or more bits per pixel — like the color round display — needs dramatically more of that RAM just to hold one frame. See Display & Coordinate Systems for the full walkthrough.

Why does the color round display need far more frame buffer memory than the monochrome OLED?

The gap comes from two multiplying factors: total pixel count and Bit Depth. The 128x64 monochrome OLED has 128 times 64, or 8,192, total pixels, and each pixel needs just 1 bit (on or off), giving a Display Buffer Memory of 1,024 bytes, as its frame buffer size calculation shows. The 240x240 color round display has 240 times 240, or 57,600, total pixels — nearly seven times as many — and under the RGB565 Color Model each of those pixels needs a full 16 bits (2 bytes) to represent thousands of possible colors rather than just on or off. Multiplying those two factors together, 57,600 pixels times 2 bytes per pixel, gives roughly 115,200 bytes needed for one complete color frame buffer, well over 100 times the OLED's 1,024 bytes. This Memory Use Comparison matters directly for project planning, since the RP2040 has a fixed total amount of RAM shared across the whole program; a large color frame buffer leaves noticeably less memory available for animation logic, sprites, or other program data than the OLED's tiny buffer does. It's a concrete example of the color versus mono trade-off: richer color and resolution cost real, measurable RAM. See Display & Coordinate Systems for the underlying size calculations.

What is the difference between the Rectangle Method and the Filled Rectangle Method?

The Rectangle Method, called as rect(x, y, w, h, c), draws only the outline of a rectangle — a hollow border at the given position, width, and height, with the interior left untouched. The Filled Rectangle Method, called as fill_rect(x, y, w, h, c), instead colors every pixel inside that same width and height, producing a solid block rather than just a border. Both are FrameBuffer methods supplied by the FrameBuf module, and both take the same four positional arguments plus a color value, so switching between them is as simple as choosing which method name to call. In practice, rect() is useful for outlining a screen border or drawing a hollow shape, while fill_rect() is the faster way to block in a solid area, such as a rectangular cheek highlight or a solid-color background patch. This is a specific case of the broader Filled Versus Outlined Shape distinction that also applies to ellipse(), where a boolean fill argument toggles between an outline and a solid interior — but for rectangles, MicroPython simply provides two separate, dedicated methods instead of one method with a fill flag. See Basic Drawing Primitives for both methods in action.

What does the transparent color key parameter do when calling the blit method?

The Blit Method, called as blit(source, x, y, key), copies another frame buffer's pixels onto the current one at position (x, y) — but without the optional key argument, every pixel from the source image gets copied, including any background pixels around the actual artwork, which would cover up whatever was underneath. The Transparent Color Key solves this: it's an optional color value passed as that fourth argument that tells blit() to skip copying any source pixel matching that color, leaving the destination's existing pixel untouched at that spot instead. For example, if a small round pupil sprite is drawn on a black background and black is passed as the transparent color key, blitting that sprite onto an eye copies only the visible pupil pixels, letting the white of the eye underneath show through everywhere the sprite's background would otherwise have been. Without a transparent color key, that same blit would paste a black square over the eye, hiding it completely. This technique is essential for stamping small pre-drawn sprites, like pupils or highlights, onto a larger scene without covering up everything around them. See Basic Drawing Primitives for a full walkthrough of blit and sprites.

Which MicroPython release first added the ellipse() and poly() methods to FrameBuffer?

The MicroPython V1.20.0 Release, shipped in April 2023, was the first official stable release to include ellipse() and poly() as standard FrameBuffer methods available to every user without building custom firmware. Before that point, developers had to work with the Framebuf Method Set Before V1.20, which included only fill, pixel, hline, vline, line, rect, text, scroll, and blit — meaning any curved shape, like a round pupil, had to be faked using Approximating Curves With Lines or custom pixel-by-pixel routines. The underlying code for ellipse() and poly() was actually written and merged into MicroPython's source tree earlier, in August 2022, through the Ellipse Poly Dev Branch Merge, and adventurous developers could try the new methods early by flashing Nightly Build Firmware onto their own boards. But it wasn't until v1.20.0 became the official stable release that ordinary students and hobbyists could count on ellipse() and poly() being present on a stock Pico. A later patch, the Ellipse Bug Fix V1.24.1, subsequently corrected a rendering defect in the method's quadrant fill behavior. This course assumes v1.20.0 or newer. See A History of MicroPython's FrameBuf Drawing Support for the complete timeline.

How does the quadrant fill code control which part of an ellipse is drawn?

The Quadrant Fill Code is the optional mask parameter m of the Ellipse Method, ellipse(x, y, xr, yr, c, f, m), and it uses a 4-bit value to choose which of the ellipse's four quarters actually get drawn or filled, rather than the whole oval. Each of the four bits corresponds to one Coordinate Plane Quadrant around the ellipse's center — top-left, top-right, bottom-left, and bottom-right — and setting or clearing a bit turns that quarter's drawing on or off. This makes the quadrant fill code the key trick behind many robot face features: passing a mask that selects only the two bottom quarters produces a Bottom-Half Mouth Curve, a simple curved smile shape made from just half an ellipse, while selecting only a top-left quadrant can create a small raised-eyebrow arc instead of a full closed loop. Individual quadrant flags are typically combined using a Bitwise Operator like | (OR) to select more than one quarter at once. Without the quadrant fill code, a program calling ellipse() would always get a complete oval, forcing developers back to slower workarounds like drawing separate line segments to fake a half-curve. See Ellipse & Polygon Drawing for worked examples.

What is the difference between a convex polygon and a concave polygon?

A Convex Polygon is a shape in which every interior angle measures less than 180 degrees, meaning that any straight line segment drawn between two points inside the shape stays entirely inside it — a simple triangular eyebrow drawn with the poly method is a convex polygon, since it has no dents anywhere along its outline. A Concave Polygon instead has at least one interior angle greater than 180 degrees, which creates a visible dent or notch in the shape's outline; a jagged, lightning-bolt-shaped eyebrow would be a concave polygon, because a line between two of its points could pass briefly outside the shape at the notch. Both types can be drawn with the same poly() method — MicroPython's FrameBuffer doesn't restrict poly() to convex shapes — the difference is purely geometric, based on the angles formed by the vertices supplied in the point array. Recognizing the difference matters when designing an eyebrow or other angular feature: convex shapes tend to read as smoother and calmer, while concave, notched shapes can add a sharper, more jagged visual character to an expression like anger or surprise. See Ellipse & Polygon Drawing for how both are constructed with poly().

What is a point array, and why does the poly() method require one?

A Point Array is the array.array object of alternating x and y coordinate values that the Poly Method requires to know where each vertex, or corner, of a polygon should be placed. Rather than accepting a plain Python list of separate coordinate pairs, poly(x, y, coords, c, f) expects coords to be an array.array built with a type code like 'h' (signed 16-bit integers), because that format is compact and fast for MicroPython's FrameBuffer code to read directly on a memory-constrained microcontroller. For example, array('h', [0,0, 10,0, 5,-8]) defines three vertices — (0,0), (10,0), and (5,-8) — that together outline a triangular eyebrow shape, with the values read two at a time as x, y, x, y, and so on. The point array's coordinates are offsets from the position given by poly()'s own x and y arguments, so the whole shape can be moved just by changing those two numbers rather than rebuilding the entire point array. Because of the Closed Polygon Path behavior, the array only needs to list each vertex once — poly() automatically connects the last point back to the first to close the outline. See Ellipse & Polygon Drawing for more point array examples.

What is the difference between digital input reading and analog input reading?

Digital Input Reading checks a pin's value as one of exactly two states, high or low, which is exactly what's needed to detect whether a push button is currently pressed or released — the pin reads one of only two possible values, nothing in between. Analog Input Reading instead measures a pin's value across a continuous range, which is necessary for a component like a potentiometer whose knob produces a smoothly varying voltage rather than a simple on/off signal. Behind the scenes, analog input reading depends on Analog-To-Digital Conversion, where the RP2040's built-in ADC hardware translates that continuous voltage into a discrete numeric value, typically ranging from 0 to 65535, that MicroPython code can work with directly. In practice, a momentary push button used to change a robot's expression is read with digital input reading, returning a simple pressed-or-not result, while a potentiometer used to smoothly adjust an eyebrow angle in real time is read with analog input reading, returning a number that changes gradually as the knob turns. Choosing the wrong reading method for a given component either loses meaningful detail (treating a potentiometer as digital) or adds unnecessary complexity (treating a button as analog). See Interactive Controls: Inputs & Concurrency for code examples of both.

What is the difference between a rotary encoder and a potentiometer as an input device?

A Potentiometer is a variable resistor with a knob that produces a changing voltage depending on its physical position, read by the microcontroller through Analog Input Reading as an absolute value — turning the knob to a specific position always produces roughly the same reading, similar to a volume dial with a defined minimum and maximum. A Rotary Encoder looks similar but works completely differently: rather than reporting an absolute position, it reports discrete clockwise or counterclockwise steps as it turns, letting a program track relative movement and direction instead of a fixed voltage level. This means a rotary encoder can spin freely and indefinitely in either direction, which makes it well suited for browsing an Expression Selection Menu one item at a time, since each detected step simply moves the selection up or down regardless of how many times the knob has been turned before. A potentiometer, by contrast, is better suited to a parameter that has a natural minimum and maximum, like sweeping an eyebrow angle smoothly across its full usable range. In short: use a potentiometer when a control needs a fixed range and absolute position, and use a rotary encoder when a control needs open-ended, step-by-step relative movement. See Hardware & Electronics Foundations for how both are wired.

How does the RGB565 color model pack red, green, and blue values into 16 bits?

The RGB565 Color Model is a 16-bit color format used by the color round display that packs three separate color channels into a single number: 5 bits for red, 6 bits for green, and 5 bits for blue, adding up to exactly 16 bits total. Green gets one extra bit compared to red and blue because human eyes are more sensitive to variations in green light, so that extra bit lets the display represent slightly finer shades of green than of the other two colors. Building a packed RGB565 value from separate red, green, and blue numbers requires Bit Shifting to move each channel's bits into its correct position within the 16-bit number — for instance, shifting a red value left by 11 places positions it correctly at the very top of the number, ahead of the green and blue bits. This single packed number is what gets passed as the color argument to drawing methods like fill_rect() or ellipse() on the color round display, in contrast to the Monochrome Color Model used by the OLED, where a drawing color value is simply 0 or 1. See Porting Faces to a Color Display for the full breakdown of RGB565.

What does the color565() function do?

The Color565 Function is a helper function that converts three separate red, green, and blue values into a single packed 16-bit number formatted according to the RGB565 Color Model, ready to be passed directly to a drawing method's color argument. Rather than manually bit-shifting and combining red, green, and blue values by hand every time a new color is needed, a program can simply call something like color565(255, 100, 50), which returns one 16-bit number representing that specific orange color, suitable for use in a call to fill_rect() or ellipse() on the color round display. This function is typically provided by the display driver library rather than written from scratch, since the exact bit-packing math (5 bits red, 6 bits green, 5 bits blue) is easy to get wrong by hand. Using color565() also makes drawing code more readable, since a program can define Named Color Constants like ORANGE = color565(255, 100, 50) once near the top of a script and then reuse that readable name everywhere a specific color is needed, rather than repeating a raw hexadecimal number throughout the code. See Porting Faces to a Color Display for examples using color565().

What is the difference between sleep function timing and the non-blocking delay pattern?

Sleep Function Timing uses MicroPython's time.sleep() or sleep_ms() to pause a program's execution entirely for a fixed duration — simple to write, such as sleep_ms(200) to wait 200 milliseconds between animation frames, but it completely freezes the program during that pause, so nothing else, including checking a button, can happen until the sleep finishes. The Non-Blocking Delay Pattern solves this limitation by checking elapsed time on every pass through the main loop instead of pausing execution, typically using time.ticks_ms() to read the current time and time.ticks_diff() to calculate how much time has passed since a stored reference point. If enough time has elapsed, the program performs the delayed action, such as advancing a blink; if not, the loop simply continues on to check other things, like a button's state, before coming back around. The practical consequence is significant: a robot face using sleep function timing to control a blink would become completely unresponsive to button presses for the duration of every sleep call, while one using the non-blocking delay pattern can keep reading buttons and updating other animations continuously, even while an Animation State Timer is counting down toward the next blink. This is why the non-blocking pattern is essential for any animation loop that must also stay responsive to input. See Animating Expressions: Timing & Motion for the full technique.

What is the difference between an Import Statement, Comment Syntax, and Docstring Convention?

MicroPython programs use three different ways to add supporting text to code, and it's easy to confuse them. An Import Statement is a line of code that loads a module's functions and classes into a program so they can be used, such as import framebuf bringing in the FrameBuffer class before any drawing can happen. Comment Syntax refers to text marked with a leading # that MicroPython ignores completely when running the code, existing purely so a human reader understands what a line does. The Docstring Convention is a more structured form of documentation: a short description written as the first statement inside a function, using triple quotes, explaining what the function does for later readers. The key difference is that an Import Statement changes what code is available to run, while Comment Syntax and the Docstring Convention are both purely for human readers and have zero effect on program behavior.

Example: def draw_face(mood): """Draws a complete robot face for the given mood.""" follows the Docstring Convention, while # clear the screen first uses ordinary Comment Syntax.

See MicroPython Fundamentals I and MicroPython Fundamentals II.

How does Aspect Ratio differ from Circular Display Geometry?

Aspect Ratio describes the proportional relationship between a display's width and height. The 128x64 monochrome OLED has a wider-than-tall aspect ratio, while the 240x240 color round display has a perfectly square one, and this changes how much horizontal versus vertical room a face design has to work with. Circular Display Geometry is a related but separate idea specific to the round display: even though its pixel grid is a square, the visible image area is a circle inscribed within that square, so content drawn near the corners is hidden behind the round bezel. A robot face designed for the round screen needs to respect circular display geometry by keeping features clustered near the center, on top of already adapting to its square aspect ratio.

Example: Designing for the OLED's aspect ratio means keeping eyes close together horizontally, while designing for the round display also means keeping every feature away from the corners the Circular Display Geometry hides.

See Display & Coordinate Systems for a full comparison of the two displays.

What roles do the SPI Clock Line and SPI Data Line play on the RP2040?

The RP2040 Microcontroller is the dual-core ARM chip at the heart of the Raspberry Pi Pico, and it provides the built-in SPI hardware this course's color display relies on. When it talks to the GC9A01 driver, it uses two wires that are essential to every transfer. The SPI Clock Line carries a timing signal from the microcontroller that synchronizes exactly when each bit of pixel data should be read by the display — without it, the display has no way to know when one bit ends and the next begins. The SPI Data Line carries the actual stream of bits, such as pixel color values, from the microcontroller to the display. Both lines run alongside the chip-select and data-command lines on the same SPI bus.

Example: If the SPI Clock Line is disconnected, the color display receives no usable data even though power and the SPI Data Line are both connected correctly, because it has no way to time when to sample each incoming bit.

See Hardware & Electronics Foundations for the full wiring diagram.

What do the Fill Method, Horizontal Line Method, and Vertical Line Method each do?

Three of the simplest FrameBuffer drawing methods each serve one specific job. The Fill Method, called as fill(c), sets every single pixel in the buffer to one color value, typically clearing the screen to black before any new expression is drawn. The Horizontal Line Method, called as hline(x, y, w, c), draws a straight line of w pixels wide starting at (x, y) and extending rightward. The Vertical Line Method, called as vline(x, y, h, c), draws a straight line of h pixels tall starting at (x, y) and extending downward. All three are faster than a general line-drawing call because each is optimized for one specific direction and doesn't need to calculate an angle.

Example: oled.fill(0) clears the screen, and a following oled.hline(40, 40, 20, 1) draws a flat, neutral mouth as a single straight stroke.

See Basic Drawing Primitives for the complete set of drawing methods.

What are Blit Palette Mapping, the Scroll Method, and the Unsigned Byte Array used for?

Blit Palette Mapping is an optional feature of the blit method that uses a separate palette frame buffer to translate a source image's pixel values into different destination colors during the copy — it can recolor a single grayscale pupil sprite differently each time it's blitted. Underneath every FrameBuffer object sits an Unsigned Byte Array — the bytearray or bytes object used to store raw pixel values in memory, forming the actual storage a FrameBuffer object wraps. The Scroll Method, called as scroll(dx, dy), shifts the entire buffer's pixel content by a horizontal and vertical offset, leaving vacated pixels unchanged rather than clearing them automatically.

Example: The Scroll Method can slide a scanning-eye animation sideways across the OLED without redrawing it from scratch, though the vacated pixels left behind must still be filled in manually.

See Basic Drawing Primitives for how these extend basic drawing into sprites and motion.

What is the difference between the Ellipse Radius Parameter and a Polygon Vertex?

The Ellipse Radius Parameter refers to the xr and yr arguments of the ellipse() method, which independently set the oval's horizontal and vertical half-widths. Setting a larger yr than xr stretches a pupil into a tall oval instead of a circle. A Polygon Vertex is a different building block entirely: one corner point of a polygon, defined by an x and y coordinate, that together with other vertices outlines a shape passed to the poly() method. Where the Ellipse Radius Parameter controls a smooth curve with just two numbers, a polygon needs a full list of Polygon Vertex coordinates to define its exact outline, so rounded features like eyes use the former while angular features like a sharp eyebrow rely on the latter.

Example: A round pupil only needs its Ellipse Radius Parameter values, while a triangular eyebrow needs three separate Polygon Vertex points passed to poly().

See Ellipse & Polygon Drawing for worked examples of both.

How does Facial Action Coding relate to Emotion Recognition Accuracy?

Facial Action Coding is a detailed system for describing facial expressions in terms of individual muscle movements, used by researchers to precisely analyze which specific features signal which emotions, rather than just saying an expression "looks angry." Emotion Recognition Accuracy is a measure of how reliably viewers correctly identify the intended emotion from a drawn or displayed expression, used to evaluate and refine a robot face design. A designer can apply Facial Action Coding style thinking — asking which specific feature movement is actually communicating the emotion — while trying to improve poor Emotion Recognition Accuracy on a confusing expression.

Example: If classmates keep misreading a sad expression as tired, a designer using Facial Action Coding might find the eyebrows aren't tilted distinctly enough, adjust that feature, then re-test Emotion Recognition Accuracy with a fresh audience.

See Emotion Theory & the Core Expression Set for how these ideas ground the course's designs.

What features distinguish a Sad Expression from a Surprised Expression?

The Sad Expression and the Surprised Expression sit at opposite ends of several drawn features. A Sad Expression is typically drawn with a downward-curved mouth and eyebrows angled upward at their inner corners, near the nose, giving a drooping look. A Surprised Expression, by contrast, is drawn with very wide eyes, high raised eyebrows, and a small open circular mouth. Where the Sad Expression's eyebrows tilt toward the center and down, the Surprised Expression's eyebrows lift straight up and away from the eyes, and the mouths differ just as sharply between a downward curve and a small round opening.

Example: Keeping eye size identical for a neutral baseline, changing only eyebrow angle and mouth shape is enough to shift the same face from a Sad Expression to a Surprised Expression.

See Emotion Theory & the Core Expression Set for the full recipe of all thirteen expressions.

How are the Afraid Expression and the Disgusted Expression each drawn?

The Afraid Expression signals fear and is typically drawn with wide, enlarged eyes, raised eyebrows, and a slightly open mouth. The Disgusted Expression signals revulsion and looks quite different: it typically combines a scrunched or asymmetrical mouth with lowered, tightened eyebrows. Both share dramatic feature values compared to neutral, but they diverge in an important way: the Afraid Expression stays fairly symmetrical, with both eyes and eyebrows changing together, while the Disgusted Expression often deliberately breaks facial symmetry to suggest a physical recoiling reaction.

Example: The Afraid Expression can be built by enlarging the eye size parameter and raising both eyebrows, while the Disgusted Expression might pair a lopsided mouth shape with one eyebrow drawn lower than the other.

See Emotion Theory & the Core Expression Set for the drawing recipe of each.

What was the Vector Companion App, and how did it fit the Consumer Robotics Market?

The Vector Companion App was the smartphone application used to set up, monitor, and customize Anki Vector, connecting the robot to Wi-Fi and cloud voice services. Anki Vector, like Cozmo before it, was sold into the broader Consumer Robotics Market — the overall industry and customer base for robots sold directly to individuals and families rather than businesses or governments. The Vector Companion App reflects a pattern common across the consumer robotics market: pairing a physical robot with a connected app to add voice-assistant features and account-based customization. That dependency on cloud services also meant Vector's usefulness relied on Anki's servers staying online.

Example: A user might open the Vector Companion App just to check the robot's battery level or review its daily activity log, a feature common to many products across the Consumer Robotics Market.

See A History of Screen-Based Robot Faces.

What was the Buddy Mobile Robot Base, and how did a Crowdfunding Campaign fund it?

The Buddy Mobile Robot Base was the wheeled locomotion platform beneath Blue Frog Robotics Buddy that let the robot navigate a home independently while its screen displayed a face. Unlike Cozmo or Vector, which stayed in one place, Buddy's mobile base meant it was designed to roll from room to room checking on family members. Blue Frog Robotics funded Buddy's development partly through a Crowdfunding Campaign — a public fundraising effort, typically run on a platform like Kickstarter, where a company pre-sells a product to backers to fund development before mass production. A crowdfunding campaign creates public pressure and a fixed shipping expectation that can be hard to meet.

Example: Backers who supported the Crowdfunding Campaign expected the Buddy Mobile Robot Base and its screen face to ship on a specific timeline, and delays became a well-documented part of Buddy's story.

See A History of Screen-Based Robot Faces.

Common Challenge Questions

Why did my OLED display stop working after I wired it up?

The single most common way to permanently destroy a display is connecting its power pin to the wrong voltage source. Before you power on any display for the first time, trace the red power wire back to the exact pin it plugs into on the Pico or Maker board and confirm it reads 3V3, not VBUS or VSYS. VBUS carries the raw 5 volts coming in from USB, and VSYS is the board's main system rail — both can be higher voltage than a small OLED or color display is designed to handle. Feed either of those into a display's power pin and the display can be damaged instantly and permanently, with no warning and no way to undo it.

This mistake is especially painful because displays are the most expensive single component in the kit — far more costly than a button, resistor, or jumper wire. A few seconds of double-checking before you flip the power on can save you from having to buy a replacement.

The fix is simple and should become a habit: every time you build a new circuit or move wires around, physically trace the red wire from the display back to its source pin and read the silkscreen label next to that pin. Don't assume it's correct because it looks right — confirm the label says 3V3 every single time before applying power. See Keeping Displays Safe: Power and Mounting for the full wiring walkthrough.

Why does my MicroPython code throw an IndentationError?

Python (and MicroPython) doesn't use curly braces {} to mark blocks of code the way many other languages do. Instead, it uses whitespace itself as grammar: every line that belongs inside the same if statement, loop, or function must be indented by exactly the same number of spaces. If one line in a block has four spaces and the next has five, or if you accidentally mix tabs and spaces, MicroPython can't tell which lines belong together and raises an IndentationError.

For example, this fails:

1
2
3
if temperature > 30:
    print("hot")
     print("turn on fan")   # one extra space — error!

This is one of the most common first bugs almost every new Python programmer hits at least once, so if it happens to you, you are in very good company — it doesn't mean you did something unusually wrong.

To fix it, look at the exact line Thonny points to in the error message and compare its leading whitespace to the line above it. They need to line up precisely. The easiest long-term fix is to let Thonny's auto-indent feature handle the counting for you: when you press Enter after a line ending in a colon (:), Thonny automatically indents the next line correctly, and pressing Backspace or Tab keeps blocks aligned. Avoid manually retyping indentation from scratch, since that's exactly where mismatched spaces sneak in. See Indentation Rules: Python's Silent Grammar for more.

Why doesn't my global variable update inside a function?

This is one of the sneakiest bugs a beginning Python programmer can run into, because MicroPython never warns you that anything went wrong. Here's what's happening: if you assign a value to a variable name anywhere inside a function — even once — Python treats that name as a brand-new local variable that only exists inside that function, even if a global variable with the exact same name already exists outside it. The function quietly creates and updates its own private copy, while the global variable sitting outside the function never gets touched.

For example:

1
2
3
4
5
6
7
blink_count = 0

def update_blink():
    blink_count = blink_count + 1  # creates a NEW local variable

update_blink()
print(blink_count)   # still 0!

You might expect blink_count to increase, but it stays at 0 forever, because the blink_count inside update_blink() is a completely different variable from the one defined outside.

The fix is to explicitly tell Python you mean the global one, using the global keyword before you assign to it:

1
2
3
def update_blink():
    global blink_count
    blink_count = blink_count + 1

Whenever a variable seems to silently "reset" or "not update" even though your code looks correct, check whether you're assigning to it inside a function without declaring it global first. See Global Versus Local Scope for a full walkthrough of this bug.

Why does my eyebrow show up at the bottom of the screen instead of the top?

This mix-up happens to almost everyone the first time they draw on a screen. In math class, you learned that the Y-axis increases as you move up the page, with the origin (0, 0) in the bottom-left corner. Screens work the opposite way: the origin sits in the upper-left corner, and Y values get larger as you move down the screen, not smaller. So a small Y value like 5 is near the top of the display, and a large Y value like 60 is near the bottom.

If you're picturing a face and place an eyebrow using a Y value based on "higher number means higher up" habit from math class, it will actually land near the bottom of the screen instead of near the top — because you flipped the direction out of old habit.

The fix is simple once you know to look for it: whenever a shape appears vertically flipped from where you expected — showing up low when you wanted it high, or vice versa — check your Y coordinate first. Remind yourself that "down the screen" means "increasing Y," and recalculate your intended position using distance from the top edge, not distance from an imagined bottom-left origin. Sketching your layout on graph paper with (0,0) marked in the top-left corner before you start coding can help this rule stick. See The Origin Sits in the Upper-Left Corner for a deeper look at the screen coordinate system.

Why does my sprite have an invisible box around it after I blit it onto the screen?

The blit() method copies pixels from one frame buffer into another one rectangle — the sprite's full bounding box — at a time, including every pixel in that rectangle, not just the visible shape inside it. If your sprite is round or irregularly shaped, the rectangle around it still contains background-colored pixels in the corners and edges. Without telling blit() to skip those, it copies them too, and they land on top of whatever was already drawn on the screen — creating a faint rectangular patch around your sprite wherever the sprite's background color didn't match the screen's actual background.

This is a very common mistake: the sprite's shape itself looks perfectly correct, so it's easy to overlook the ghost box surrounding it, especially at a glance.

The fix is to pass a transparent color key as an extra argument to blit() — a specific color value that tells the method "skip copying any pixel that matches this color." Set the color key to whatever background color your sprite's bounding box uses, and those pixels get skipped instead of overwriting the real background. If a sprite ever looks like it's sitting inside an invisible box, that's the signal to check whether you left out the color-key argument. See Making Backgrounds Show Through: The Transparent Color Key for the full explanation.

Why did adding quadrant bit values draw the wrong half of my ellipse?

The quadrant fill code used to control which parts of an ellipse get filled works by combining bit values, where each bit is tied to one fixed screen quadrant: upper-left, upper-right, lower-left, and lower-right. A very common mistake is picturing "the left half" as "the first and last quadrant" and adding those two bit values together — for example, 1 + 8. That combination doesn't draw the left side at all; it actually draws upper-right and lower-right, which is the right side of the shape.

This happens because the bit values are locked to actual screen positions, not to whatever side you're mentally picturing as "first" or "last." Your intuition about which quadrant comes "first" doesn't match how the quadrants are numbered in the fill code.

To avoid this, don't guess or rely on memory when combining quadrant bits. Look up each quadrant's actual screen position in the reference table before adding bit values together, confirm which specific quadrants (upper-left, upper-right, lower-left, lower-right) you actually want filled, and only then add their corresponding bit values. If your ellipse fills the opposite side from what you intended, that's a strong sign you swapped which quadrant a bit value represents — go back to the table and re-check each one individually before combining them again. See The Real Trick: Quadrant Fill Code for the quadrant reference table.

Are Ekman's six universal emotions scientifically proven facts?

Not exactly — they're a well-studied and highly influential theory, but not a settled, undisputed law of nature. Psychologist Paul Ekman's research proposed that a small set of emotions (like happy, sad, angry, and afraid) are expressed and recognized the same way across essentially all human cultures. That work has shaped decades of emotion research and is a genuinely useful, research-backed starting point for designing robot expressions — which is exactly why this book builds its core expression set around it.

However, some psychologists question exactly how universal certain expressions really are across every culture, and how much surrounding context changes what a facial expression actually communicates to an observer. A raised eyebrow might read as "surprised" in one situation and "skeptical" in another, depending on what's happening around it — evidence that facial expressions alone don't always tell the whole story.

The practical takeaway for a robot face designer is to treat Ekman's universal emotions as a strong, evidence-based foundation to design from — not as the final, unchallengeable word on how human emotion works. It's completely reasonable to use this framework as your starting point while also recognizing that real emotional expression is more complicated and more debated among researchers than a simple six-item list might suggest.

Why does my robot's expression look great on my desk but unreadable from across the room?

It's easy to fall into this trap because of how you naturally test your own work: you sit close to your laptop or the display, maybe six inches away, tweak an expression until it looks great, and move on. But a robot rarely gets to live at six inches from its audience. A classroom robot, a hallway greeter, or a demo table often needs to communicate clearly to someone standing much farther away — sometimes from across an entire room.

An expression that reads perfectly up close can lose all of its clarity at distance. Fine details — a subtle eyebrow tilt, a slightly curved mouth — shrink to almost nothing on a small display once you're several feet away, and what registered as "curious" up close might just look like "blank" from across the room. Lighting matters too: a classroom's overhead lights can wash out contrast that looked fine on your desk.

The fix is to build testing at realistic distance into your workflow, not just testing at arm's length. Once an expression looks right up close, back away — walk to the far side of the room, or have a friend look at the screen from where the robot will actually be seen — and check whether you can still tell what emotion it's showing. If it stops being clear at a distance, consider making key features (like eyebrow angle or mouth curvature) larger, bolder, or higher-contrast. See Viewing Distance Readability and Classroom Lighting Consideration for more.

Why did my robot miss a button press during an animation?

If your code calls time.sleep() for any noticeable length of time — say, time.sleep(2) for a two-second pause — the processor is completely frozen for that entire duration. It can't check a button, read a sensor, or respond to anything happening in the outside world, because it isn't running any other code at all; it's just waiting. If a student presses a button at exactly the wrong moment, the robot simply never notices, because the processor was mid-nap and never checked. The longer the sleep() call, the longer the robot is effectively deaf and blind to everything around it.

This is easy to run into because sleep() feels like the obvious, simple way to make something wait — pause an animation frame, wait between blinks, and so on. The problem only becomes visible once you add buttons or other inputs that need to be checked frequently.

The fix is to replace long blocking sleep() calls with a non-blocking delay pattern: instead of freezing the whole program, you record a timestamp (using a ticks function), and on each pass through your main loop you check whether enough time has passed to trigger the next animation step. Between those checks, the loop is free to also check for button presses or other input, so nothing gets missed. If your robot seems to "ignore" input during animations, look for a sleep() call sitting in the middle of that code. See The Catch: Non-Blocking Delay Pattern for the fix in detail.

Why does my display show scrambled garbage when I speed up the SPI clock?

Above roughly 20 MHz, the part of your circuit that breaks first usually isn't the microcontroller or the display chip — it's the breadboard and jumper wires connecting them. Long jumper wires act like tiny antennas and add resistance and capacitance that a fast clock signal doesn't tolerate well. At higher clock speeds, the sharp, clean pulses meant to carry data start blurring together into a fuzzy, indistinct signal by the time they reach the display. The display can't correctly read that blurred signal, and the visible result is a screen full of scrambled, garbled pixels instead of your intended image.

This is a hardware limitation, not a mistake in your MicroPython code — your drawing logic can be perfectly correct and you'll still see garbage on screen if the physical connection can't keep up with the speed you requested.

To fix it, keep your clock and data wires as short as possible, and run a ground wire directly alongside them to help stabilize the signal. If a display starts misbehaving after you raise the SPI bus speed, the most reliable first fix is simply to drop the baud rate back down until the screen behaves correctly again, then only push the speed back up if you shorten or improve your wiring first. See Three Different Ceilings for more on hardware speed limits.

Why does my robot freeze or act strangely after I added a button interrupt handler?

An interrupt handler is special: it can fire in the middle of whatever your main loop is doing, pause that work immediately, run the handler, and only then let the main loop continue. That makes interrupt handlers powerful for catching a button press instantly — but also dangerous if the handler itself is slow.

If your interrupt handler directly calls something like draw_face() or fb.show() — both of which take real time to run — that slow work now blocks whatever the main loop was doing, in exactly the same way a long sleep() call would. Since the interrupt can fire at any unpredictable moment, this can cause stuttering, dropped frames, or the program appearing to freeze or behave erratically, and the bug can be hard to track down because it doesn't happen the same way every time.

The fix is to keep interrupt handlers as short as possible. Instead of doing the real work inside the handler, have it simply set a flag — a variable that just records "a button was pressed." Then, in the main loop, check that flag on each pass and do the actual slow work (redrawing the face, updating the display) only when the main loop is ready for it. This keeps the interrupt handler fast and lets the main loop stay in control of when slower operations actually run. See Keep Interrupt Handlers Short for the full pattern.

Why does pressing my menu button once skip two or three expressions?

Mechanical buttons don't switch cleanly from "not pressed" to "pressed" in one clean instant. When you press a physical button, the metal contacts inside actually bounce — touching, separating, and touching again several times within a few milliseconds — before finally settling into a steady connection. If your code checks the button's state on every pass through the loop without accounting for this, it can register several bounces as several separate presses, even though a human finger only pressed it once.

That's exactly the symptom described here: one press of a "next expression" button jumps straight from "happy" to "angry," skipping right past two or three expressions in between, because every bounce edge got counted as its own button press.

The fix is called debouncing: after detecting a button press, the code waits a short, fixed amount of time (the debounce time constant, often just a few tens of milliseconds) before it's willing to register another press from that same button. This gives the mechanical bounce time to settle before the next reading counts. If your menu ever seems to have "a mind of its own" — jumping multiple steps per press, or reacting inconsistently — check your debounce logic first, since a missing or too-short debounce delay is almost always the cause. See Real Buttons Are Messy: Button Debounce for the full fix.

Why does my eyebrow disappear near the top of the round display even though my coordinates look correct?

This is the round display's version of the Y-axis trap from earlier in the book: code that looks completely correct on screen — right coordinates, right shape, right size — can still fail on real hardware, because nothing in the frame buffer itself stops you from drawing outside the visible circle.

A round display's frame buffer is still stored as a square block of memory underneath, even though only a circle inscribed inside that square is actually visible once the display is assembled — the corners of the square exist in memory but are physically covered by the display's round bezel and never light up. If a feature's bounding box — like an eyebrow positioned near the top of the screen — drifts even slightly outside that inscribed circle, the frame buffer happily accepts those pixel coordinates without any error or warning. The data is there, correctly stored, but the glass in that corner region simply never lights up, so the feature appears to vanish or get cut off.

The fix is to double-check that every feature's full bounding box — not just its center point — stays within the visible inscribed circle, especially for features placed near the top, bottom, or sides of the layout. Measure or calculate the circle's radius and keep drawing coordinates safely inside it, particularly for anything near the four "corners" of the square buffer. See Round Display Layout for more on designing within a circular display.

Best Practice Questions

When should I use ellipse() instead of poly() for a face feature?

Reach for ellipse() whenever a feature is naturally round or oval — eyes, pupils, cheeks, and most mouth curves all fit this shape. Reach for poly() when a feature needs straight edges or an asymmetric silhouette that an ellipse can't fake, like a triangular eyebrow, a angular jaw outline, or a zig-zag mouth for "confused." A good rule of thumb: if you can describe the shape using a width and a height, use ellipse(); if you need to describe it as a list of specific corner points, use poly(). Mixing both is normal and expected — a single expressive face might use ellipse() for two eyes and poly() for a sharp, angry eyebrow. Don't force a round tool onto an angular job just because ellipse() feels easier to call; the resulting shape will look mushy instead of expressive. This course's triangle eyebrow example is a good model: a triangle reads as "angry" specifically because its straight edges create a sharp point, something quadrant fill code on an ellipse cannot produce.

How do I decide which features to include in a first expression set?

Start smaller than you think you need to. Build the smallest set of expressions that would still count as a finished, working face — often just neutral, happy, and sad — get that set solid, and only then decide what to add next. This mirrors a pattern you'll see across the whole course: Miko, the educational robot discussed in Chapter 2, succeeded partly because it kept its moving parts simple and let its screen do the emotional work instead of piling on features. When you're scoping your own face, ask which features earn their cost and which ones just look impressive without adding real expressiveness. Subtle expressions like contempt are a good example of something to defer: a one-sided mouth raise is easy to lose on a small display, and it's smarter to nail the other twelve core expressions first. You can always layer in harder expressions once your basic set reads clearly at a normal viewing distance. A finished, working three-expression face beats an ambitious thirteen-expression face that never quite gets there.

Should I test a new line of MicroPython code in a saved program first?

No — test it in the REPL (Read-Eval-Print Loop) first. The REPL is the interactive line at the bottom of Thonny's Shell panel where you can type a single line of MicroPython and see the result immediately, without saving or running a whole file. If you're not sure what fb.ellipse(64, 32, 20, 15, 1, True, 0x0f) will actually draw, type it into the Shell panel and watch what happens on the display before you paste it into your saved program. The REPL answers in under a second, and that habit saves far more debugging time than it costs, because you catch mistakes — a swapped x and y coordinate, or a fill flag left as False — before they're buried inside forty lines of code. This is especially useful early in the course, covered in Chapter 3, while you're still learning how functions like ellipse() and poly() expect their arguments. Treat the REPL as your sketchpad: quick experiments belong there, and only code you've already confirmed works belongs in your saved .py file.

When is it worth blitting a sprite instead of drawing shapes live?

Use blit() to copy a pre-built sprite when a feature never changes shape and speed matters more than flexibility — a fixed background pattern, a static logo, or a complex bitmap that would take many separate drawing calls to recreate every frame. Use live drawing primitives like ellipse() and poly() when a feature needs to change parameters from frame to frame, such as an eyebrow angle shifting during an animation or a pupil moving during a gaze. The trade-off is flexibility versus performance: a sprite copies pixels in one fast operation but can't be reshaped without a completely different sprite, while ellipse() and poly() recompute the shape every call but respond instantly to a changed parameter. A practical middle ground many robot faces use: draw the parameterized features (eyes, eyebrows, mouth) live so they can animate, and blit a static background or decorative sprite behind them. See the blit method section for how transparent color keys let a blitted sprite sit on top of other drawing without blocking it out with a solid rectangle.

How do I choose between the monochrome OLED and the color round display?

Choose the 128x64 monochrome OLED when your design goal is fast, simple, high-contrast expressions and you want features that pop clearly in any lighting — its on/off pixels are easy to read from across a classroom. Choose the 240x240 color round display when your design goal depends on color itself carrying meaning, such as a red flush for anger or a blue tint for calm, or when a circular face shape better matches your robot's personality. The two displays also differ in memory and speed: the monochrome buffer packs one bit per pixel, so it's smaller and faster to redraw, while the color buffer needs two bytes per pixel under the RGB565 color model, which costs more memory and more time per frame. If your design leans on color emotion association — using warm colors for excitement and cool colors for sadness — the color display is worth that extra cost. If your design's expressiveness comes entirely from shape and motion, the monochrome display delivers that just as well for less overhead. See Chapter 15's trade-off discussion for a fuller comparison of display performance and memory use.

What's the best order to draw a face's features in each frame?

Draw features back to front, the same way a painter layers a canvas: background or face outline first, then larger features like cheeks or eyelids, and finally the smallest, most detailed features like pupils and highlights last. This draw order matters because later drawing calls overdraw — paint over — anything already on screen at the same coordinates. If you draw a pupil before its eye, the eye shape will cover the pupil completely, and it will vanish. A well-planned draw order also lets you skip unnecessary work: if you know a mouth will always be redrawn every frame anyway, there's no benefit to clearing that exact region separately first. Chapter 6's section on draw call order optimization walks through exactly this kind of layering decision. As a habit, sketch your feature list in back-to-front order before you write a single draw_face() call — eyebrows, eyes, pupils, mouth, in that order for most faces — so you're not debugging a vanished feature after the fact.

Should I worry about blocky-looking curves on the OLED display?

No — a small amount of blockiness around a curve's edge is completely normal at this resolution, not a bug in your code. Every curve on a 128x64 monochrome OLED is built from square pixels, and there's no built-in anti-aliasing (smoothing of jagged edges) to soften that at such a small pixel count. If your fb.ellipse() call looks a little jagged, don't spend time hunting for a mistake in your parameters — the blockiness is simply how low-resolution displays render curves. Instead, step back a few feet, roughly the distance a person talking to your robot would actually stand, and check how the curve reads from there. At a normal viewing distance the same curve reads as smooth and expressive, because human eyes stop resolving individual pixels once they're small enough relative to distance. This is a good general habit for the whole course: judge your robot's face the way a real user would experience it, not by zooming in on a screenshot. See the anti-aliasing limitation section for more on why this happens.

How should I add randomness to make animations feel more alive?

Add small, targeted random ranges to timing values rather than making every animation fully unpredictable. A single line like random.randint(2000, 5000) for the gap between blinks does real design work: it's the difference between a face that feels like a wind-up toy on a fixed metronome and one that feels like it's actually paying attention to the room. The trick is scope — randomize when a small, expected motion happens, not whether it happens or how large it is. A blink should still always be a blink; only its timing needs variation. Apply this same idea sparingly elsewhere: a slight random offset on a gaze position or an idle animation's start delay can add personality without making the face's behavior confusing or hard to read. Overusing randomness — randomizing an expression's shape or intensity, for instance — can undercut the emotional design rubric you built the face against in the first place. See Chapter 12's section on randomized blink timing for the full pattern, including how it fits inside a non-blocking animation loop.

What's the simplest way to confirm a button press worked on screen?

Let the expression change be the confirmation — don't build a separate confirmation message, icon, or beep unless your design specifically calls for one. If pressing a "next expression" button immediately changes the face from sad to surprised, that visible change already tells the user their press registered; they never have to wonder if anything happened. This is the cheapest, most reliable form of user interface feedback available in this course's scope, because it costs no extra drawing calls, no extra state, and no extra button logic beyond what you'd build anyway. Reserve dedicated feedback elements — a small icon, a border flash — for situations where the expression change alone might be ambiguous, such as switching between two expressions that look similar on a small screen. When you're deciding how much feedback a control needs, ask whether the face's own reaction already answers the question "did that work?" If it does, resist the urge to add more. See Chapter 14's discussion of interface feedback for how this fits into a full expression menu.

How do I judge whether an expression I designed is actually readable?

Judge it the way Ekman's emotion research judges a real human expression: check whether the specific features that signal that emotion are present and clear, not just whether the face looks generally different from neutral. Eyebrows carry the most emotional signal of any single feature, followed closely by the mouth, with the eyes contributing a supporting role — so start your critique there. Ask three concrete questions: does this expression stay recognizable at the distance a real viewer would stand from the robot, does it still read correctly under normal classroom lighting, and could it be confused with a different expression in your set (expression ambiguity)? A useful practical test is showing the expression to someone who hasn't seen your code and asking them to name the emotion with no hints — if they consistently guess wrong or hesitate, the design needs stronger signaling on eyebrows or mouth curvature before you move on. This rubric-based approach, covered in Chapter 11 on expression design and readability, turns "does this look right to me" into a repeatable check grounded in actual emotion research rather than personal taste.

Why should I check my MicroPython version before copying display code online?

Because ellipse() and poly() haven't always existed in framebuf, and code written for an older or different MicroPython build can look correct while quietly failing on your Pico. If you find MicroPython code online that draws circles using a loop full of trigonometry instead of a single fb.ellipse() call, that code isn't wrong — it's most likely older than v1.20.0 (April 2023), the version this course assumes, or written for a device still running older firmware. Before assuming any drawing function is available, check sys.implementation.version in the REPL and compare it against what a function actually needs. This habit matters beyond just avoiding confusion: it protects you from spending an hour debugging an AttributeError that has nothing to do with your logic and everything to do with a version mismatch. It's also a useful lens for reading any hardware tutorial online — always ask what platform and firmware version the author was actually using. See Chapter 8's version history for the full timeline of when each drawing feature landed.

When does packing color values with bit shifting actually pay off?

It pays off any time you need to store a full color as a single compact number instead of three separate variables — which is exactly what the RGB565 color model requires on the color round display. The color565() function you'll build in Chapter 15 uses bit shifting and the | (bitwise OR) operator to pack a red, green, and blue value into one 16-bit number, with each color shifted into its own reserved slot. You already have every operator this trick needs from Chapter 4's bitwise operators section — this is simply that same idea applied to a real, useful problem. Outside of color packing, bit shifting is worth reaching for whenever you need to combine several small values into one number efficiently, rather than tracking them as separate variables that have to stay in sync. For most face-drawing code you won't need bitwise tricks elsewhere, so don't force them into logic where a normal variable or a small function would be clearer — save this technique specifically for the color-packing problem it was built to solve.

Should I try to give my display a resolution that isn't a multiple of 8?

No — stick to display resolutions where both dimensions are multiples of 8, the same pattern you'll see on every real display this course covers: 128x64, 96x16, and 128x32 all follow it. This isn't a marketing coincidence; it's byte alignment working in the manufacturer's favor. A frame buffer stores pixel data in bytes, and when a row's pixel count divides evenly by 8, every row packs into whole bytes with nothing wasted or split awkwardly across a byte boundary. If you were ever designing custom display hardware or a custom sprite size, this is the rule to follow: dimensions that are multiples of 8 keep your frame buffer size calculation clean and your memory use predictable. For this course, you won't need to choose your own display resolution, but the same logic applies when you're sizing a sprite or bitmap for blit() — keeping its width a multiple of 8 avoids subtle padding bugs in how the bytes are packed. See Chapter 5's section on byte alignment for the full explanation of how bytes and pixels map to each other.

Advanced Topic Questions

How would you divide animation and input-polling work across the RP2040's two cores?

The RP2040 has two identical processor cores, and MicroPython's _thread module lets a program hand one of them an entirely separate job. The most natural split for a robot face keeps core task assignment simple: dedicate core 0 to the animation loop — clearing the frame buffer, calling draw_face(), and pushing pixels with .show() — while core 1 does nothing but poll a button and a potentiometer, exactly the arrangement demonstrated in Interactive Controls: Inputs & Concurrency.

For example, _thread.start_new_thread(core1_input_task, ()) starts a tight while True: loop on core 1 that checks button.value() and pot.read_u16() every 10 milliseconds and updates shared variables like current_mode or eyebrow_angle, while core 0's loop never has to pause drawing to check a pin. This matters because a slow drawing call — several ellipse() calls for a busy expression — can no longer make the program late in noticing a button press, and vice versa.

The trade-off is that both cores can now touch the same global state at once, so the split only works if each core sticks to a clearly defined job: one core mostly writes a variable, the other mostly reads it, and neither core does anything slow inside code the other depends on. Dual-core processing is entirely optional for this course — a well-written single-core polling loop is enough for nearly every robot face project — but splitting animation from input this way is the cleanest use of the second core when timing gets tight.

When does blitting a pre-rendered sprite outperform redrawing primitives?

Blitting — copying a pre-rendered bitmap into the frame buffer with blit() — wins whenever the same shape needs to appear, unchanged, many times in a row, because a memory copy is dramatically cheaper than recomputing an ellipse() or poly() from scratch on every frame. The draw time benchmarking covered in Animating Expressions: Timing & Motion makes this concrete with real ticks_us() measurements: a full frame redrawn entirely with drawing primitives measured around 1,150 microseconds, while a small blitted eyelid sprite used for a blink measured only about 190 microseconds — roughly six times faster for that one feature.

The catch is that blitting only pays off for shapes that stay geometrically identical between appearances. A blink is a perfect case: the closed-eyelid shape is exactly the same every time it appears, so it can be pre-rendered once and copied repeatedly. A continuously changing shape — pupils smoothly tracking a moving gaze target, or an eyebrow easing through ten distinct angles — would need a separate pre-rendered sprite for every intermediate position, which trades drawing-time savings for a much larger memory bill of stored bitmaps. The strongest designs mix both: primitives for parts that move through many unique positions, blitting for parts — like a repeated blink shape or a static face outline — that redraw identically many times, plus partial updates that only push the rows of the buffer that actually changed.

What race conditions can occur when both RP2040 cores share one state variable?

Cooperative multitasking on the RP2040 works because MicroPython does not force safety on shared data — it only works if the program's own discipline keeps both cores from colliding. A global state variable like current_mode, described in Interactive Controls: Inputs & Concurrency, is exactly the kind of value that can go wrong under dual-core processing: if core 1's input-polling task advances current_mode from GAZE_MODE to ADJUST_MODE at the same instant core 0 is reading that variable to decide what to draw, core 0 could act on a value that is only partway updated, or read the old value and act on stale information for one extra frame.

In practice this usually shows up as a subtle visual glitch rather than a crash — a single frame drawn using the wrong mode's logic, or a potentiometer reading briefly applied to the wrong parameter — because MicroPython integers are small enough that most single-variable reads and writes complete without interruption, but relying on that becomes fragile once more than one variable needs to change together in sync.

The safer pattern keeps the relationship one-directional and small: let one core own writing a variable and the other core only read it, avoid a slow operation like draw_face() running while the other core is mid-update on data it depends on, and keep shared state to a single mode integer rather than a whole dictionary being edited from two places at once.

Should a capstone's dedicated input core use polling or an interrupt handler?

Combining dual-core processing with a button interrupt handler sounds like the ultimate responsiveness upgrade, but Interactive Controls: Inputs & Concurrency shows why the answer is usually "polling is enough" once a core is already dedicated entirely to input. Interrupts exist to solve a specific problem: a single busy core that can't afford to keep stopping what it's doing just to check a pin. That problem disappears once core task assignment gives input its own core with nothing else to do — a polling input loop checking button.value() every 10 milliseconds on a core doing nothing else will catch essentially every realistic button press, without an interrupt handler's extra complexity: debouncing logic, a flag variable, and handler code that can fire in the middle of anything else running on that core.

Interrupts still earn their keep in a narrower case: if that same core also needs to do something else occasionally — reading a second sensor, or running a light animation of its own — an interrupt-driven button keeps that other work from delaying detection of a press. But for the common capstone pattern of "core 0 animates, core 1 only reads controls," stacking an interrupt handler on top of an already-dedicated polling core adds real complexity — new opportunities for the handler and the polling loop to disagree about state — for a responsiveness gain nobody watching the demonstration will notice. Simpler code that's easier to debug live, in front of a class, usually wins that trade-off.

Is the color round display worth its memory and speed cost for a robot's personality?

Porting Faces to a Color Display lays out the numbers plainly: the GC9A01's RGB565 frame buffer needs 115,200 bytes against the OLED's 1,024, consumes roughly 43 percent of the RP2040's 264 KB of RAM by itself, and takes about 112 times longer to push over SPI on every .show() call. Deciding whether that cost is worth paying is a real design trade-off analysis, not a question with one correct answer — it depends entirely on what the robot needs to communicate.

A robot whose personality depends on subtle emotional nuance — a calm companion that signals mood with a soft color-cycling glow layered under its expressions, or one that leans on color emotion association (warm reds for alertness, cool blues for calm) as a second channel alongside shape — gets real value from the extra 65,536 colors and circular layout, and the slower redraws are an acceptable cost for that richness. A robot that needs to blink and react as fast as possible on a tight memory budget, or one where a design team trusts monochrome shape-based readability more than color-palette design, is better served staying on the OLED, where the buffer leaves plenty of RAM free and every .show() call is nearly instantaneous. The strongest capstones name this trade-off explicitly rather than defaulting to color because it "looks nicer" — and stronger still, as Chapter 16 notes, are the ones that port their design to both displays and let the comparison speak for itself.

How do you judge whether a capstone's expression set has become overcomplicated?

Computational Thinking & Capstone Design names this tension directly: expressiveness versus complexity is the trade-off between adding more visual richness and keeping a project simple enough to finish, debug, and demonstrate with confidence. Judging where a specific design falls on that spectrum is an evaluate-level skill, and it comes down to testing against concrete criteria rather than a gut feeling.

A few checks separate a well-scoped expression set from an overcomplicated one. First, does every expression still pass the readability rubric at classroom distance — can a viewer identify it within a couple of seconds, and is it clearly distinct from its nearest confusable neighbor? A design that adds a ninth or tenth expression overlapping visually with existing ones adds risk without adding real communication. Second, has every state actually been tested on real hardware, including the transitions between them — a twenty-expression capstone demoed for only six of those states is not more expressive than a ten-expression one that works reliably end to end; it just has more places left to fail during a live demonstration. Third, does the idle animation and control scheme stay responsive as more states get added, or does polling more inputs and drawing more variations visibly slow the frame rate?

A concrete comparison makes this real: a ten-expression capstone with a clean, fully rehearsed control cycle is a stronger project than a twenty-expression one that crashes mid-presentation because half its states were never fully tested — exactly the trade-off Chapter 16 uses to make this concept concrete.

How could you extend the draw_face() parameter system beyond this course's expressions?

The face_state dictionary and draw_face(fb, state) function built starting in Chapter 9 are deliberately open-ended — Computational Thinking & Capstone Design points out that this abstraction was never meant to stop at three parameters. Extending the system means adding new keys to face_state and new logic inside draw_face() that reads them, without breaking anything that already works — the same code reuse discipline the whole book has been building toward.

A few concrete directions fit comfortably within this course's FrameBuf and MicroPython scope. Independent left and right eyebrow angles, instead of one shared value, unlock asymmetric expressions like a single raised, skeptical eyebrow. A gaze_x, gaze_y pair, separate from pupil size, lets pupils drift toward a simulated point of interest instead of only opening and closing. A blush_color or accent-color key, meaningful only on the color display, layers color-emotion association on top of existing shape parameters rather than replacing them. Even expression blending — computing a face_state dictionary that sits partway between two named expressions by averaging their parameter values frame by frame — extends the animation chapter's timing patterns into smoother, more original transitions than a simple three-frame blink.

The important habit is architectural: every extension should still flow through one draw_face() function reading one state dictionary — the modularity this book has practiced since Chapter 9 — rather than a growing pile of special-case drawing functions for each new idea.

How would you combine animation, controls, and color into one cohesive capstone?

A strong capstone doesn't bolt these systems together — it lets them share the same underlying face_state and mode logic so they read as one coherent personality rather than three separate demos stapled into one program. The mode state machine from Interactive Controls: Inputs & Concurrency is a natural backbone: a button press cycles current_mode through named states, and every other system reacts to whichever mode is currently active.

Consider a concrete example: an idle "resting" mode plays the blink-and-gaze animation loop from Animating Expressions: Timing & Motion while a slow color cycling animation — the color_wheel() function from Porting Faces to a Color Display advancing a few degrees per frame — tints the background a gently shifting hue, signaling "thinking" without a single triggered expression. A button press moves to an "alert" mode, where the animation freezes on a single sharp expression and the background snaps to a fixed warning-red named color constant, reinforcing the shape-based signal with a matching color instead of a shifting one. A potentiometer, active only in a third mode, live-tunes eyebrow angle exactly as the interactive-controls chapter demonstrated, letting a visitor see the parameter system in action.

Porting that whole design to both displays, using the cross-display code compatibility built into the parameter system since Chapter 9, is what makes it read as one original robot personality rather than two unrelated projects — the same face_state values and mode logic, with only color constants and driver code changing between the OLED and the round display, exactly the integration Computational Thinking & Capstone Design's capstone checklist asks for.