Week 04 Embedded Programming
Assignments
Group assignment:
Demonstrate and compare the toolchains and development workflows for available embedded architectures Document your work to the group work page and reflect on your individual page what you learned
Individual assignment:
Browse through the datasheet for a microcontroller Write and test a program for an embedded system using a microcontroller to interact (with local input &/or output devices) and communicate (with remote wired or wireless connections)
Learning outcomes
Implement programming protocols.
Have you answered these questions?
Linked to the group assignment page
Browsed and documented some information from a microcontroller's datasheet
Programmed a board to interact and communicate
Described the programming process(es) you used
Included your source code
Included ‘hero shot(s)’
Week 4: Embedded Programming — Individual Assignment
Overview
This week was a fun one (they have all been fun, but this was the less stressful fun)! Most of my work was conversing with Claude AI and working through issues, so at the end I asked it to write up what we had worked on and I'll add to it.
This week I worked with three different microcontrollers, each with a different architecture, toolchain, and programming approach:
| XIAO RP2040 | XIAO ESP32-C6 | ATtiny412 | Raspberry Pi Pico H | |
|---|---|---|---|---|
| Architecture | 32-bit ARM Cortex-M0+ (dual-core) | 32-bit RISC-V (single-core) | 8-bit AVR | 32-bit ARM Cortex-M0+ (dual-core) |
| Clock | 133 MHz | 160 MHz | 20 MHz | 133 MHz |
| RAM | 264 KB | 512 KB | 256 bytes | 264 KB |
| Flash | 2 MB | 4 MB | 4 KB | 2MB |
| Wireless | None | WiFi 6, BLE 5, Zigbee | None | None |
| Language | MicroPython | Arduino C++ | Arduino C++ | MicroPython |
| IDE/Toolchain | VS Code + mpremote | Arduino IDE | Arduino IDE + megaTinyCore | VS Code + mpremote |
| Firmware Method | Drag-drop .uf2 | Arduino IDE direct upload | SerialUPDI via FTDI adapter | Drag-drop .uf2 |
| Communication | Wired (USB serial) | Wireless (WiFi web server) | UPDI (single wire) | Wired (USB serial) |
| Status | Complete | Complete | Complete | Complete |
Board 1: XIAO RP2040
Board: Seeed XIAO RP2040
Language: MicroPython
IDE: VS Code with mpremote
Environment Setup
For this project I used VS Code as my text editor and mpremote as the command-line tool for uploading code to the board.
Hardware
- Seeed XIAO RP2040 microcontroller board
- USB-C cable for power and programming
- Breadboard and jumper wires
Flashing MicroPython Firmware
Before I could write any code, I needed to install MicroPython on the board. The XIAO RP2040 uses a drag-and-drop method — no command-line tools required.
- Downloaded the firmware — I grabbed the latest MicroPython
.uf2file from micropython.org/download/RPI_PICO. - Entered bootloader mode — I held down the BOOT button on the board, then plugged in the USB-C cable while still holding the button. After releasing, the board appeared on my Mac as a USB flash drive called RPI-RP2.
- Dragged and dropped — I dragged the
.uf2file onto the RPI-RP2 drive. The board automatically rebooted with MicroPython installed and the USB drive disappeared. That was it.
The whole process took less than a minute. Per Claude AI, the RP2040 has a built-in USB bootloader in ROM that makes the chip appear as a mass storage device, which is why it's so simple compared to boards like the ESP32 that require command-line flashing tools.
Setting Up the Development Environment
I used VS Code with mpremote because Mr. Dubick had a class on how to get us started. Here's how we set it up:
-
Created a Python virtual environment for the project (I did this part on my own because I have other python projects that I want to keep separate):
-
Installed mpremote inside the virtual environment:
-
Configured a VS Code task to automate uploading. I created a
tasks.jsonfile so that uploading code is as simple as Terminal → Run Task → "Upload to Pico". The task runs:
I ran into command not found: mpremote errors in the beginning because the VS Code task wasn't activating the virtual environment before running the command. The fix was making sure the task included source .../myenv/bin/activate && before the mpremote command. (Resolved by Claude AI)
Software Summary
- VS Code — Text editor for writing
main.py - mpremote — Command-line tool that uploads code to the board
- tasks.json — VS Code task configuration that automates the upload command
- MicroPython firmware — Flashed to the board via
.uf2file
XIAO RP2040 Pinout
With the USB connector facing up, I mapped out the pin layout from the board's silkscreen labels:
GPIO Numbers vs. Pin Labels
This was causing problems with the code working initially. In MicroPython, Pin(n) refers to the GPIO number, NOT the board's silkscreen label. The pin labeled D0 on the board is GPIO26, so in code I must use Pin(26), not Pin(0). Using Pin(0) actually accesses GPIO0, which is physical pin D6. I learned my lesson. Keep the pinout diagram close by whenever I'm wiring and coding.
Additional Board Notes
- All GPIO pins are 3.3V — do not input more than 3.3V or the CPU may be damaged
- All 11 digital pins support PWM
- The onboard LEDs are active LOW — writing LOW (0) turns them ON, and writing HIGH (1) turns them OFF
Exercise 1: LED Blink
My first test was to confirm the board was working, the firmware was flashed correctly, and my upload workflow was functional. Pin 25 controls the onboard user LED.
Code
from machine import Pin
import time
print("Hello World")
led = Pin(25, Pin.OUT)
while True:
led.toggle()
time.sleep(0.5)
Result
The onboard LED alternated between blue and green at half-second intervals. The red power LED remained steady. This confirmed the board, firmware, and upload pipeline were all working.
Exercise 2: Slide Switch Input
For my next step I wanted to introduce digital input (it was actually Claude's idea). I used a 3-pin slide switch to control the onboard LED.
How a Slide Switch Works
Per Claude AI, a 3-pin slide switch has a common (middle) pin that connects to one of the two outer pins depending on the slide position. I initially assumed the common pin meant ground, but it's actually called "common" because it is shared by both circuit paths. I decided what each pin connects to in my circuit design.
Wiring
- Common (middle pin) → GPIO26 (D0 on the board)
- Outer pin (one side) → 3V3
- Outer pin (other side) → GND
Claude AI informed me of this: Because the 3-pin switch always connects the GPIO to either 3V3 or GND, the pin always has a defined state. There is no floating pin problem, so no pull-up or pull-down resistor is needed.
Code
from machine import Pin
print("Hello World")
led = Pin(25, Pin.OUT)
switch0 = Pin(26, Pin.IN)
while True:
print(switch0.value())
if switch0.value() == 0:
led.value(0) # LED ON (active LOW)
else:
led.value(1) # LED OFF (active LOW)
The logic works like this: when the switch is on the GND side, the pin reads 0, so led.value(0) turns the LED ON. When the switch is on the 3V3 side, the pin reads 1, so led.value(1) turns the LED OFF.
Debugging: The GPIO Pin Number Bug
This is where I hit a wall. After wiring everything up, the switch had no effect on the LED. I added a print() statement to the loop and saw the pin was stuck reading 0 regardless of switch position. I grabbed my multimeter and confirmed the switch was outputting the correct voltage — 3.3V on one side, 0V on the other — and that the voltage was reaching the correct breadboard row.
The problem turned out to be in my code. I had written Pin(0) because the board silkscreen says "D0." But in MicroPython, Pin(0) refers to GPIO0, which is physical pin D6 on the XIAO — a completely different pin on the opposite side of the board from where my switch was wired. Changing the code to Pin(26) (the actual GPIO number for D0) fixed the issue immediately.
This was a frustrating bug but an excellent lesson. I now always cross-reference the board label against the GPIO number in the pinout table before writing any code.
Exercise 3: Momentary Button Input
After getting the slide switch working, I moved on to a momentary pushbutton to learn about pull-up resistors (this was Claude AI's suggestion)
Understanding the Button
My button has 4 legs. I asked Claude to help me understand how it workss. He suggested I use my multimeter in continuity mode to understand how they're connected: the legs on opposite sides of the button are permanently connected (they beep without pressing). The legs on the same side only connect when the button is pressed. So the 4-leg button is really just a 2-terminal switch — the extra legs are for stability on the breadboard.
The Floating Pin Problem
Claude gave me this information because I sure did not know to ask it: Unlike the 3-pin slide switch which always connects the GPIO to either 3V3 or GND, a momentary button with only 2 connections leaves the GPIO connected to nothing when the button is not pressed. Interesting. So I learned that a floating (disconnected) pin picks up random electrical noise and reads unpredictable values — sometimes 0, sometimes 1. That is very good to know!
Pull-Up Resistors
Per Claude AI: A pull-up resistor connects the pin to 3V3, "pulling" it up to a known HIGH state when nothing else is driving it. When the button is pressed and connects the pin to GND, GND overpowers the weak resistor and the pin reads LOW. The RP2040 has internal pull-up and pull-down resistors that can be enabled in code with Pin.PULL_UP, so I didn't need to add a physical resistor to the circuit.
Wiring
- One leg → GPIO27 (D1 on the board)
- Other leg (opposite side) → GND
- No 3V3 connection needed — the internal pull-up handles it

Code
from machine import Pin
print("Hello World")
led = Pin(25, Pin.OUT)
button = Pin(27, Pin.IN, Pin.PULL_UP)
while True:
print(button.value())
if button.value() == 0:
led.value(0) # LED ON (active LOW)
else:
led.value(1) # LED OFF (active LOW)
When the button is not pressed, the pull-up holds the pin HIGH (reads 1) and the LED stays off. When I press the button, GND overpowers the pull-up, the pin reads 0, and the LED turns on. The LED only stays on while I hold the button down.
Switch vs. Button Behavior
I noticed a key difference between the two inputs: the slide switch holds its position and gives a persistent state, while the momentary button only registers while pressed and then springs back. If I wanted the button to toggle the LED (press once for on, press again for off), I would need to track state in the code and handle debouncing — something to explore in a future exercise.
Exercise 4: PWM Fading (External LED)
Next I wanted to go beyond simple on/off and smoothly control LED brightness using Pulse Width Modulation (PWM). This required switching to an external LED circuit.
What is PWM?
An LED is either on or off — it doesn't truly "dim." PWM tricks your eyes by switching the LED on and off thousands of times per second, varying the ratio of on-time to off-time in each cycle. This ratio is called the duty cycle. At 0% the LED appears off, at 50% it appears half brightness, and at 100% it appears fully on.
The RP2040 uses a 16-bit value for the duty cycle, ranging from 0 (off) to 65535 (full brightness). 65535 is the maximum value of a 16-bit unsigned integer (2^16 - 1), giving 65,536 steps of brightness resolution.
Why an External LED?
I first tried PWM on the onboard LED (pin 25), but it just toggled between blue and green instead of fading smoothly. The onboard LED is wired internally in a way that doesn't support variable brightness through PWM. I needed to wire up an external LED on a GPIO pin for proper analog-style dimming.
Wiring
- GPIO26 (D0) → LED long leg (anode/positive)
- LED short leg (cathode/negative) → resistor (220Ω–470Ω) → GND
The resistor limits current to protect the LED.
Troubleshooting: Finding the Right Resistor
My first attempt didn't work — the LED barely glowed. I checked the resistor with my multimeter in resistance mode and discovered I had grabbed a 1MΩ resistor by mistake. At 1MΩ, almost no current flows through the LED. I swapped it for a resistor in the 220Ω–470Ω range and the LED lit up properly.
Troubleshooting: PWM Needs a Frequency
Even with the correct resistor, my first PWM fade code produced no visible change on the LED. I dropped into the REPL and tested line-by-line to isolate the problem:
from machine import Pin, PWM
led = PWM(Pin(26))
led.freq(1000) # This was the missing line
led.duty_u16(65535) # Full brightness — confirmed PWM works
led.duty_u16(32768) # Half brightness — confirmed fading works
led.duty_u16(5000) # Dim — confirmed range works
The issue was that PWM(Pin(n)) creates the PWM object but doesn't output any signal until you explicitly set a frequency with .freq(). Once I added Led.freq(1000) to my code, the fade started working. Once again, thanks AI.
Code
from machine import Pin, PWM # type: ignore
import time
Led = PWM(Pin(26))
Led.freq(1000)
while True:
for i in range(0, 65535, 500):
Led.duty_u16(i)
time.sleep(.01)
print(i)
time.sleep(1.5)
for i in range(65535, 0, -500):
Led.duty_u16(i)
time.sleep(.01)
print(i)
time.sleep(1.5)
How the Code Works
PWM(Pin(26)) creates a PWM output on GPIO26 (board label D0) and Led.freq(1000) sets the frequency to 1kHz. The first for loop fades from off to full brightness by stepping the duty cycle from 0 to 65535 in increments of 500. The time.sleep(.01) holds each brightness level briefly so the fade is visible. At peak brightness, the code pauses for 1.5 seconds, then the second for loop fades back to off using a negative step of -500. print(i) outputs the current duty cycle value to serial so I can monitor the progression.
Tuning the Fade
Getting the fade to look right took some experimentation. I started with a step size of 100, but the brightness changes were too subtle to see clearly. Increasing to 500 made the difference much more visible. The sleep time inside the loop controls the overall speed of the fade — smaller values make it faster, larger values make it slower. The 1.5 second pause between fades is separate and controls how long the LED holds at full brightness or full off before reversing.
Lessons Learned
- The onboard LED (pin 25) doesn't fade — it only toggles between colors. I needed an external LED for PWM.
- PWM requires
freq()to be set — without it, no signal is output even if the duty cycle is set. - Resistor values matter — 1MΩ is far too high for an LED circuit. 220Ω–470Ω is the correct range for 3.3V.
- Test incrementally — I learned to confirm basic on/off works with
Pin.OUTbefore adding PWM complexity. - External LEDs are not active LOW — unlike the onboard LED, a standard external LED turns on with HIGH and off with LOW.
Serial Communication & Debugging
Viewing Serial Output
One thing that confused me early on was that my print("Hello World") statement never appeared anywhere. The reason is that the mpremote upload command pushes the code and resets the board but does not keep a serial connection open afterward. The print runs on the board with no listener.
To see serial output, I followed this process:
- Upload code using Terminal → Run Task → "Upload to Pico"
- Open a new terminal in VS Code and run:
mpremote repl - Press Ctrl+C to interrupt the running code (the
>>>prompt appears) - Press Ctrl+D to soft-reboot, which reruns
main.pyfrom the beginning — nowprint()output appears in real time - Use Ctrl+] or Ctrl+X to exit the REPL when done
Debugging Techniques I Used
Throughout these exercises I relied on several debugging approaches. Here's a summary of what worked and when:
| Technique | When I Used It | What It Revealed |
|---|---|---|
print(value) in loop |
Switch code ran but LED didn't respond | Pin was stuck reading 0 — pointed to wrong GPIO |
mpremote repl + Ctrl+C/D |
Needed to see serial output from the board | Confirmed code was running and printing values |
| Multimeter (voltage mode) | Suspected wiring issue with switch | Traced 3.3V/0V from switch terminals to GPIO row |
| Multimeter (continuity) | Needed to understand button wiring | Mapped which button leg pairs are permanently connected |
| Multimeter (resistance) | LED barely glowed with PWM | Found I had a 1MΩ resistor instead of 220Ω |
| Simple test code first | PWM fade wasn't working | led.value(1) confirmed the LED and wiring were fine |
| REPL line-by-line | PWM object wasn't producing output | Discovered .freq() must be set before PWM works |
Exercise 5: Bidirectional Serial Communication
For the final RP2040 exercise, I needed to demonstrate wired communication between the board and a remote device. I built a serial command interface where my Mac sends text commands to the RP2040 over USB, and the board parses them, acts on them, and sends responses back.
Concept
All my previous exercises used print() to send data one way — from the board to my Mac. This exercise adds the other direction: my Mac sends commands to the board using sys.stdin, and the board reads, interprets, and responds to them. This is bidirectional UART communication over the USB connection.
Wiring
Same as before — just the external LED and button:
- GPIO26 (D0) → LED → resistor (220–470Ω) → GND
- GPIO27 (D1) → button leg, opposite leg → GND (internal pull-up in code)
Code
from machine import Pin, PWM
import time
import sys
import select
# Setup
led = PWM(Pin(26))
led.freq(1000)
led.duty_u16(0)
button = Pin(27, Pin.IN, Pin.PULL_UP)
commands = "Commands: on, off, fade, blink, status, help"
print("RP2040 Serial Command Interface")
print(commands)
while True:
# Check if there's incoming serial data
if select.select([sys.stdin], [], [], 0)[0]:
command = sys.stdin.readline().strip().lower()
if command == "on":
led.duty_u16(65535)
print("LED: ON")
elif command == "off":
led.duty_u16(0)
print("LED: OFF")
elif command == "fade":
print("LED: Fading...")
for i in range(0, 65535, 500):
led.duty_u16(i)
time.sleep(.01)
for i in range(65535, 0, -500):
led.duty_u16(i)
time.sleep(.01)
led.duty_u16(0)
print("LED: Fade complete")
elif command == "blink":
print("LED: Blinking 3 times...")
for i in range(3):
led.duty_u16(65535)
time.sleep(0.5)
led.duty_u16(0)
time.sleep(0.5)
print("LED: Blink complete")
elif command == "status":
btn = "PRESSED" if button.value() == 0 else "NOT PRESSED"
print("Button: " + btn)
elif command == "help":
print(commands)
else:
print("Unknown command: " + command)
time.sleep(0.1)
What I Added
After getting the initial version working with on, off, fade, and status, I wanted to demonstrate that I understood the code well enough to extend it on my own. I added:
blinkcommand — blinks the LED 3 times using aforloop withrange(3), toggling the PWM duty cycle between full on and off with half-second delays.helpcommand — reprints the available commands.commandsvariable — I created a single variable to hold the command list string so it's used by both the startup message and thehelpcommand. This way if I add a new command, I only update it in one place.
How to Test
- Upload with Run Task
- Open a second terminal:
mpremote connect auto repl - Press Ctrl+C then Ctrl+D to restart cleanly
- Type commands and press Enter:
on,off,fade,blink,status,help
One thing to note, When typing commands, the characters don't appear on screen as you type — the board is reading directly from sys.stdin so there's no echo. The command and response appear together after you press Enter.
What This Demonstrates
This exercise satisfies the communication requirement for the assignment. The RP2040 is exchanging data bidirectionally over wired USB serial with my Mac:
- Mac → RP2040: Text commands parsed by the board
- RP2040 → Mac: Status responses and confirmation messages
- Local interaction: LED output (PWM) and button input (digital read with pull-up)
The select.select() function is key — it checks for incoming serial data without blocking the main loop, which means the board can handle other tasks (like monitoring the button) while waiting for commands.
Board 2: XIAO ESP32-C6
Board: Seeed XIAO ESP32-C6
Language: Arduino C++ (Arduino IDE)
Communication: WiFi Web Server
About the Board
The XIAO ESP32-C6 is a RISC-V 32-bit microcontroller running at 160 MHz with 512 KB SRAM and 4 MB flash. Its standout feature is built-in wireless connectivity — WiFi 6, Bluetooth 5 LE, and Zigbee/Thread support. This makes it the go-to choice for IoT applications where the device needs network connectivity.
Environment Setup
For this board I used the Arduino IDE instead of MicroPython + VS Code. My class set up these boards with Arduino, so I followed that same toolchain.
Prerequisites
The Arduino IDE was already installed and configured from earlier classwork. The ESP32 board package was already added via the Board Manager (search "esp32" → install "esp32 by Espressif Systems"). The board URL in Preferences is:
Board Selection
In the Arduino IDE board dropdown, I selected XIAO_ESP32C6 and the corresponding port (/dev/cu.usbmodem11101).
Arduino vs. MicroPython
Using Arduino C++ on the ESP32-C6 after using MicroPython on the RP2040 gave me experience with two different programming approaches for embedded systems:
- MicroPython — Write and upload a
.pyfile, no compilation step, interactive REPL for testing - Arduino C++ — Write a
.inosketch, compiled before upload,setup()runs once andloop()runs forever
The core concepts (GPIO, digital read/write, serial output) are the same — just different syntax and workflow.
Exercise 1: LED Blink
My first test was confirming the board, IDE, and upload pipeline were all working. The ESP32-C6's built-in LED is on GPIO 15 and is active LOW — LOW turns it ON, HIGH turns it OFF. I'm actually getting a little overwhelmed with the different nuances of the boards. I'm assuming this is why documentation is stressed in the class.
Code
const int ledPin = 15;
void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(115200);
}
void loop() {
digitalWrite(ledPin, LOW); // LED ON (active LOW)
Serial.println("LED ON");
delay(500);
digitalWrite(ledPin, HIGH); // LED OFF (active LOW)
Serial.println("LED OFF");
delay(500);
}
Result
The yellow onboard LED blinked at half-second intervals. I experimented with the delay() values — changing to 200 for a fast blink and 2500 for a slow blink — to confirm I understood how the timing worked.
Troubleshooting: Port Busy
My first upload attempt failed with Could not open /dev/cu.usbmodem11101, the port is busy. The Serial Monitor was open and holding the port. Closing Serial Monitor before uploading fixed the issue. This is an important habit in Arduino — always close Serial Monitor before uploading.
Exercise 2: WiFi Web Server (Wireless Communication)
For the communication requirement, I built a WiFi web server that hosts a control page. Any device on the same network can open the page in a browser and control the LED — this demonstrates wireless communication between a remote device and the embedded system.

Concept
The ESP32-C6 connects to a WiFi network, starts an HTTP server, and serves a webpage with buttons. When I click a button on my phone or laptop, the browser sends an HTTP GET request to the ESP32. The board parses the URL, acts on the command, updates the LED, and sends back an updated webpage. This is bidirectional wireless communication.
WiFi Note
The ESP32-C6 only supports 2.4 GHz WiFi. I initially had my network set to 5 GHz only and the board couldn't connect. Turning 2.4 GHz back on fixed it. This is a hardware limitation of the chip.
Code
#include <WiFi.h>
const char* ssid = "SusieQ";
const char* password = "xxxxyyyyzzzzz";
WiFiServer server(80);
const int ledPin = 15;
String ledStatus = "OFF";
unsigned long lastToggle = 0;
bool ledOn = false;
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, HIGH); // LED off (active LOW)
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected! IP address: ");
Serial.println(WiFi.localIP());
server.begin();
}
void loop() {
WiFiClient client = server.available();
if (client) {
String request = client.readStringUntil('\r');
client.flush();
if (request.indexOf("/on") != -1) {
digitalWrite(ledPin, LOW);
ledOn = true;
ledStatus = "ON";
}
if (request.indexOf("/off") != -1) {
digitalWrite(ledPin, HIGH);
ledOn = false;
ledStatus = "OFF";
}
if (request.indexOf("/blink") != -1) {
ledStatus = "BLINK";
}
String html = "<!DOCTYPE html><html><head>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<style>body{font-family:Arial;text-align:center;margin-top:50px;}";
html += "button{font-size:24px;padding:20px 40px;margin:10px;border-radius:10px;border:none;color:white;}";
html += ".on{background:#4CAF50;} .off{background:#f44336;} .blink{background:#FF9800;}</style></head><body>";
html += "<h1>ESP32-C6 LED Control</h1>";
html += "<p>LED is currently: <strong>" + ledStatus + "</strong></p>";
html += "<a href='/on'><button class='on'>Turn ON</button></a>";
html += "<a href='/off'><button class='off'>Turn OFF</button></a>";
html += "<a href='/blink'><button class='blink'>Blink</button></a>";
html += "</body></html>";
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.println(html);
client.stop();
}
// Blink logic runs every loop pass, outside of web request handling
if (ledStatus == "BLINK") {
if (millis() - lastToggle >= 500) {
ledOn = !ledOn;
digitalWrite(ledPin, ledOn ? LOW : HIGH);
lastToggle = millis();
}
}
}
What I Added
The initial version only had ON and OFF. I added:
- Blink button — a third button on the webpage with an orange color style
- Non-blocking blink logic — instead of using
delay()in awhile(true)loop (which would freeze the board and prevent it from responding to new web requests), I usedmillis()to check if enough time has passed since the last toggle. This way the board stays responsive to web requests even while blinking. ledStatusvariable — changed from aboolto aStringso it could hold three states: "ON", "OFF", or "BLINK"
How the Non-Blocking Blink Works
My first attempt at adding blink used while(true) with delay(500) — this locked up the board so it could never check for new web requests. I couldn't send an "off" command to stop it.
The solution uses millis(), which returns the number of milliseconds since the board started. On every pass through loop(), the code checks: has 500ms passed since the last toggle? If yes, flip the LED. If no, skip it and continue checking for web requests. The board never gets stuck — it's always available to receive new commands.
The key insight is that the blink logic lives outside the if (client) block — it runs on every pass through loop(), not just when a web request comes in.
Browser Note
When accessing the web server, I had to explicitly use http:// in the URL (e.g., http://192.168.1.xxx). Firefox automatically added https:// which doesn't work since the ESP32 runs a simple HTTP server with no encryption.
What This Demonstrates
This exercise satisfies the wireless communication requirement. The ESP32-C6 is:
- Communicating wirelessly: Hosting an HTTP web server over WiFi, receiving requests and sending HTML responses
- Interacting locally: Controlling the onboard LED based on parsed URL commands
- Using a different architecture: RISC-V (vs. the RP2040's ARM), programmed in Arduino C++ (vs. MicroPython)
Board 3: ATtiny412
Chip: ATtiny412 (Microchip AVR)
Language: Arduino C++ (Arduino IDE with megaTinyCore)
Programmer: FTDI FT232RL USB-to-serial adapter via SerialUPDI
Status: Complete (using a colleague's properly soldered chip)
About the Chip
The ATtiny412 is a tiny 8-bit AVR microcontroller — a massive step down in resources from the RP2040 and ESP32-C6. It has just 4 KB of flash, 256 bytes of RAM, and 6 GPIO pins in an 8-pin SOIC package. It costs less than $1.
Despite the limitations, the ATtiny412 is a significant upgrade from the older ATtiny85 — it has hardware UART, SPI, and I2C, plus a UPDI programming interface that only requires a single wire instead of the older ISP method.
Pinout
ATtiny412
+-----------+
VCC 1| |8 GND
PA6 2| |7 PA3
PA7 3| |6 PA0 (UPDI)
PA1 4| |5 PA2
+-----------+
Environment Setup
Arduino IDE Configuration
The ATtiny412 isn't supported by Arduino IDE out of the box. I installed the megaTinyCore board package:
- In Arduino IDE, opened File → Preferences
- Added this URL to "Additional boards manager URLs":
- Opened Tools → Board → Boards Manager
- Searched for megaTinyCore and installed it
- Selected Tools → Board → megaTinyCore → ATtiny412
- Set Tools → Programmer → SerialUPDI - SLOW: 57600 baud
UPDI Programming Setup
Unlike the RP2040 (drag-and-drop .uf2) and the ESP32-C6 (Arduino IDE direct upload), the ATtiny412 has no USB connection. It's programmed through a single-wire protocol called UPDI (Unified Program and Debug Interface), which requires an external programmer.
I used an FTDI FT232RL USB-to-serial adapter as my UPDI programmer. The FTDI adapter connects to my Mac via USB and communicates with the ATtiny412 through a single data line with a resistor.
Wiring
| FTDI Pin | Connects To | Notes |
|---|---|---|
| VCC | ATtiny412 pin 1 (VCC) | Powers the chip |
| GND | ATtiny412 pin 8 (GND) | Common ground |
| TX | 5kΩ resistor → ATtiny412 pin 6 (PA0/UPDI) | Programming data line |
| RX | ATtiny412 pin 6 (PA0/UPDI) directly | UPDI is half-duplex — RX listens for responses on the same line |
Blink Code
I wrote a simple blink sketch targeting PA3 (pin 7 on the breakout, Arduino pin 4 in megaTinyCore):
const int ledPin = 4; // PA3 is Arduino pin 4 on megaTinyCore
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
}
The code compiled successfully — 446 bytes (10% of the 4 KB flash) and 10 bytes of RAM (3% of 256 bytes). The tiny footprint shows just how constrained this chip is compared to the RP2040 and ESP32-C6.
Debugging: UPDI Upload Failure
The Error
The code compiled fine, but uploading failed with:
UPDI init failed: Can't read CS register. likely wiring error.
PymcuprogError: UPDI initialisation failed
Troubleshooting Steps
- Checked wiring with multimeter — confirmed continuity on all connections between the FTDI adapter and the ATtiny412 breakout board.
- Tried different baud rates — switched between SerialUPDI SLOW (57600) and 230400 baud. Same error on both.
- Swapped TX and RX — some FTDI boards label pins from the device's perspective. Tried the resistor on RX instead of TX. Same error.
- Added RX direct connection — connected FTDI RX directly to pin 6 alongside the resistor from TX, since UPDI is half-duplex and needs to read responses. Still failed.
- Stripped wiring to minimum — removed all components except VCC, GND, and the UPDI connection. Same error.
- Tried different resistor values — swapped 5kΩ for 1kΩ. Same error.
- Tried a second serial adapter — used a DSD Tech SH-U09C5 instead of the FTDI FT232RL. Same error — ruling out the programmer as the issue.
Root Cause: Bad Solder Joint on Pin 6
After all the wiring and software troubleshooting failed, I used a multimeter to verify voltage at each chip leg while the board was powered:
- Pin 1 (VCC) to Pin 8 (GND): 5V — chip is getting power, solder joints on VCC and GND are good.
- Pin 6 (UPDI) to Pin 8 (GND): 0V — no voltage on the UPDI pin. The UPDI pin has an internal pull-up, so it should show voltage when the chip is powered. This confirmed a bad solder joint on pin 6.
I attempted to reflow the pin 6 solder joint multiple times, but the solder would not wet onto the chip leg — likely due to oxidation or damage from repeated heating.
Root cause: The SOIC chip legs are extremely thin (1.27mm pitch) and the solder was sitting on the breakout pad without actually bonding to the chip leg at pin 6. Despite looking connected visually, there was no electrical continuity between the UPDI trace and the chip.
Resolution: A colleague had a properly soldered ATtiny412 on a breakout board. Using the same FTDI adapter, wiring, and Arduino IDE configuration, the upload succeeded on the first attempt — confirming that my entire toolchain setup was correct all along. The issue was purely the solder joint on my original chip.
Lessons Learned
- Always verify with a multimeter. Visual inspection of solder joints is unreliable, especially on SOIC packages. A joint can look good and have no electrical connection.
- Test voltage at the chip legs, not just the pads. The breakout board pads can have good solder connections to the header pins while having no connection to the actual chip.
- SOIC soldering requires flux. The tiny legs need flux to help solder flow and wet properly. Without it, solder tends to ball up on the pad without grabbing the leg.
- Swap one variable at a time. By trying two different serial adapters, multiple resistor values, and multiple baud rates, I was able to isolate the problem to the physical chip/board connection rather than the programming setup.
Successful Upload and Blink
After switching to a colleague's properly soldered ATtiny412, the blink sketch uploaded immediately with no errors. The LED on PA3 (physical pin 7) blinked at half-second intervals.
Understanding the Pin Mapping
One important concept I learned is that the ATtiny412 has three different numbering systems for the same pin:
ATtiny412
┌─────────────────────┐
Pin 1 │ VCC (power) │ Pin 8 GND (ground)
│ │
Pin 2 │ PA6 → Arduino 0 │ Pin 7 PA3 → Arduino 4 ← LED HERE
│ │
Pin 3 │ PA7 → Arduino 1 │ Pin 6 PA0 (UPDI only)
│ │
Pin 4 │ PA1 → Arduino 2 │ Pin 5 PA2 → Arduino 3
└─────────────────────┘
- Physical pin 7 — the pin's position on the chip package
- PA3 — the chip's internal port/pin name from the datasheet
- Arduino pin 4 — what megaTinyCore maps it to in code
So when I wired my LED to physical pin 7 on the breakout board, I used const int ledPin = 4 in the code because that's how megaTinyCore refers to PA3. This is the same concept as the GPIO vs. board label issue I hit on the RP2040 — different naming systems pointing to the same physical connection.
What This Demonstrates
The ATtiny412 blink exercise demonstrates programming a bare microcontroller chip using an external UPDI programmer — the most complex toolchain of the three boards I used this week. Unlike the RP2040 and ESP32-C6 which have built-in USB, the ATtiny412 requires understanding the physical programming interface, wiring a serial adapter with a resistor, and navigating third-party board packages.
The debugging journey — from wiring troubleshooting to multimeter verification to discovering a bad solder joint — is a core embedded hardware skill that can't be learned from code alone.
Reflection
What I Accomplished
I successfully programmed three different microcontrollers with different toolchains, demonstrating local interaction and remote communication:
- XIAO RP2040 — MicroPython + VS Code: LED blink, slide switch input, button input with pull-ups, PWM fading, and bidirectional USB serial communication
- XIAO ESP32-C6 — Arduino C++ + Arduino IDE: LED blink with delay experimentation, WiFi web server with ON/OFF/BLINK controls using non-blocking
millis()logic - ATtiny412 — Arduino C++ + megaTinyCore + SerialUPDI: LED blink via FTDI programmer, with extensive hardware debugging that taught me more about embedded development than any of the code exercises
Key Takeaways
- Same concepts, different syntax. GPIO control, digital read/write, PWM, and serial communication work the same way conceptually across all three boards. The differences are in the toolchain, pin mappings, and language syntax.
- The multimeter is essential. Nearly every debugging breakthrough came from measuring actual voltages, continuity, or resistance — not from staring at code.
- Active LOW is everywhere. Both the XIAO RP2040 and ESP32-C6 have active LOW onboard LEDs. Understanding this early saved me debugging time later.
- Non-blocking code matters. The ESP32 WiFi server taught me why
delay()in a loop is dangerous when the board needs to handle multiple tasks. Themillis()approach is a fundamental embedded programming pattern. - Hardware failures are harder to debug than software bugs. A bad solder joint gives you the same error message as a wiring mistake, a wrong resistor, or a dead chip. You have to systematically eliminate possibilities.
Lab 1: Blink (Onboard LED)
Overview
The simplest starting point — blink the onboard LED on the Pico H. No external wiring required since the LED is internally connected to GP25.
Code
Code Explanation
Pin(25, Pin.OUT)configures GP25 as a digital output to control the onboard LED.led.toggle()flips the LED state — if it's on, it turns off, and vice versa.time.sleep(0.5)pauses for half a second between toggles, giving a 1-second full blink cycle.
Wiring
No wiring needed. The onboard LED is hardwired to GP25 on the Pico H. Just connect the board to your computer via USB.
Lab 2: Switch (Toggle Switch)
Overview
Read a toggle/slide switch to control the onboard LED. The switch stays in position, so the LED stays on or off depending on the switch state.
Code
from machine import Pin
print("Hello World")
led = Pin(25, Pin.OUT)
switch0 = Pin(26, Pin.IN, Pin.PULL_UP)
while True:
print(switch0.value())
if switch0.value() == 0:
led.value(0) # LED ON (active LOW)
else:
led.value(1) # LED OFF (active LOW)
Code Explanation
Pin(26, Pin.IN, Pin.PULL_UP)configures GP26 as a digital input with the internal pull-up resistor enabled. This pulls the pin HIGH (1) by default when the switch is open.- When the switch connects GP26 to GND, the pin reads LOW (0) and the LED turns on.
print(switch0.value())outputs the current switch state to the serial console, which is useful for debugging.
Wiring
The SPDT switch has three pins: Common (C), Normally Open (NO), and Normally Closed (NC).
| Connection | From | To |
|---|---|---|
| Switch Common (C) | — | Physical pin 31 (GP26) |
| Switch NO or NC | — | GND (any GND pin) |
| Third switch leg | — | Leave unconnected |
The internal pull-up resistor keeps GP26 HIGH when the switch is open. Flipping the switch bridges Common to GND, pulling GP26 LOW. If the on/off behavior feels backwards, move the GND wire to the other throw pin (NO ↔ NC).
Pin Reference
| Code | GPIO | Physical Pin |
|---|---|---|
Pin(25) |
GP25 (onboard LED) | Internal — no pin |
Pin(26) |
GP26 | Physical pin 31 |
Important: The GPIO number in your code (e.g., Pin(26)) does not match the physical pin number on the board. Always refer to the pinout diagram to find the correct physical pin. GP26 is physical pin 31, not the pin labeled "26" on the silkscreen.
Lab 3: Button (Momentary Push Button)
Overview
Read a momentary push button to control the onboard LED. The LED turns on only while the button is pressed.
Code
from machine import Pin
print("Hello World")
led = Pin(25, Pin.OUT)
button = Pin(27, Pin.IN, Pin.PULL_UP)
while True:
print(button.value())
if button.value() == 0:
led.value(0) # LED ON (active LOW)
else:
led.value(1) # LED OFF (active LOW)
Code Explanation
Pin(27, Pin.IN, Pin.PULL_UP)configures GP27 as a digital input with the internal pull-up resistor enabled.- A momentary button only makes contact while physically pressed, so the LED turns on during the press and turns off when released.
- The logic is the same as the switch lab — pressing the button connects GP27 to GND, pulling it LOW (0).
Wiring
A momentary button has two legs (or four legs in pairs).
| Connection | From | To |
|---|---|---|
| Button leg 1 | — | Physical pin 32 (GP27) |
| Button leg 2 | — | GND (e.g., physical pin 23 or 33) |
Any GND pin on the Pico works — they are all connected internally.
Pin Reference
| Code | GPIO | Physical Pin |
|---|---|---|
Pin(25) |
GP25 (onboard LED) | Internal — no pin |
Pin(27) |
GP27 | Physical pin 32 |
Troubleshooting Notes
GPIO vs. Physical Pin Confusion
The GPIO number used in code (Pin(26), Pin(27)) does not correspond to the physical pin number on the board. Always check the official Pico pinout diagram to find the correct physical pin. For example, GP26 is physical pin 31, and GP27 is physical pin 32.
Button Not Toggling the LED
- Check orientation: Some momentary buttons have their legs connected in pairs internally. If pressing the button does nothing, try rotating it 90° on the breadboard so the legs span across the center channel.
- Verify seating: Make sure both legs are firmly inserted into the breadboard.
- Confirm correct pins: Double-check you're wired to the correct physical pin (e.g., physical pin 32 for GP27) and a valid GND pin.
LED Behavior Seems Inverted
The code comments reference "active LOW" logic, where led.value(0) turns the LED on. The Pico H onboard LED is typically active HIGH, so if the behavior seems backwards, swap the values:
if button.value() == 0:
led.value(1) # LED ON (active HIGH)
else:
led.value(0) # LED OFF (active HIGH)
Pull-Up Resistor
If you omit Pin.PULL_UP from the input pin configuration, the pin will "float" when the button/switch is not connected to GND, giving unreliable and erratic readings. Always include Pin.PULL_UP when connecting a button or switch between a GPIO pin and GND.
Source Code Downloads
| Board | Exercise | Language | File |
|---|---|---|---|
| XIAO RP2040 | Exercise 1: LED Blink | MicroPython | rp2040_ex1_blink.py |
| XIAO RP2040 | Exercise 2: Slide Switch | MicroPython | rp2040_ex2_switch.py |
| XIAO RP2040 | Exercise 3: Button Input | MicroPython | rp2040_ex3_button.py |
| XIAO RP2040 | Exercise 4: PWM Fade | MicroPython | rp2040_ex4_pwm_fade.py |
| XIAO RP2040 | Exercise 5: Serial Commands | MicroPython | rp2040_ex5_serial_commands.py |
| XIAO ESP32-C6 | Exercise 1: LED Blink | Arduino C++ | esp32c6_ex1_blink.ino |
| XIAO ESP32-C6 | Exercise 2: WiFi Web Server | Arduino C++ | esp32c6_ex2_wifi_webserver.ino |
| ATtiny412 | LED Blink | Arduino C++ | attiny412_blink.ino |
| RPI_PICO | uf2 | RPI_PICO-20251209-v1.27.0.uf2 |