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 four boards, but only three different microcontrollers, each with a different architecture, toolchain, and programming approach. The Raspberry Pi Pico H and the XIAO RP2040 use the same RP2040 chip but have different pinouts and form factor.
| 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 | In Progess | 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, LED, switch, button 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. Very nice.
- 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. Except my Macbook didn't like it. It warned me to properly eject the device next time.
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
Mr. Dubick had a class on how to get us started and suggested we use VS Code with mpremote. 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 initially 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
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")
green = Pin(17, Pin.OUT, value=1)
red = Pin(16, Pin.OUT, value=1)
led = Pin(25, Pin.OUT)
switch0 = Pin(26, Pin.IN)
while True:
led.toggle()
time.sleep(0.5)
Result
At first the onboard LED alternated between blue and green at half-second intervals, but I eventually discovered I had to turn off red and green to get the LED to turn off when it was blue. 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 Claude suggested I introduce myself to digital input. 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 I am still getting familiar with all of the terms. 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...hmmm, okay, I'll have to take Claude's word on that one for now.
Code
from machine import Pin
print("Hello World")
green = Pin(17, Pin.OUT, value=1) #turn off green
red = Pin(16, Pin.OUT, value=1) #turn off red
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
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 frustrating, but I learned a valuable 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 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 works. 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 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. (I'd like to see that visually.) 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
- GPIO26 (D0) → LED long leg (anode/positive)
- LED short leg (cathode/negative) → resistor (220Ω–470Ω) → GND
- No 3V3 connection needed — the internal pull-up handles it
Code
from machine import Pin
print("Hello World")
led = Pin(26, Pin.OUT)
button = Pin(27, Pin.IN, Pin.PULL_UP)
while True:
print(button.value())
if button.value() == 0:
led.value(1) # LED OFF
else:
led.value(0) # LED ON (active LOW)

When the button is not pressed, the pull-up holds the pin HIGH (reads 1) and the LED stays off. When I pressed 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. 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 investigate later on.
Exercise 4: PWM Fading (External LED)
Next stop is controlling the LED brightness using Pulse Width Modulation (PWM).
What is PWM?
Per Claude AI: 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 (Future Kim now knows why). The onboard LED is wired internally in a way that doesn't support variable brightness through PWM. (That is Claude AI speaking so definitively) 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 didn't light up. I checked the resistor with my multimeter in resistance mode and discovered I had grabbed a 1MΩ resistor by mistake. I changed 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 per Claude
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. Per Claude, 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)

Serial output from RP2040
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. I can add an echo if I want to, but that will be for another day. I have other tasks to complete.
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.
ESP32-C6 Pinout
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.
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. I have to obtain the IP address of the device and enter it into my web browser and the webpage opens and initially presented two buttons. Off and On. 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"; //fake SSID
const char* password = "xxxxyyyyzzzzz"; //fake password; don't even try it
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 — an orange button on the webpage
- blink logic — instead of using
delay()in awhile(true)loop (which would freeze the board and prevent it from responding to new web requests), Claude suggested 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"
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
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
Claude gave me a simple blink sketch using 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).
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 tried to resolder the pin 6 solder joint multiple times, but it just didn't make a difference.
Root cause per Claude: 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 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 demonstrated programming a bare microcontroller chip using an external UPDI programmer. It was definitely 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.
As you have noticed I only have one exercise for this chip. I wanted to give the borrowed one back to my colleague and then solder a new one. I am waiting on the new one at this moment in time.
Board #4: Raspberry Pi Pico H
Chip: RP2040 (Raspberry Pi Foundation)
Language: MicroPython
Programmer: USB direct connection
Status: Complete
About the Chip
The Raspberry Pi Pico H is a development board built around the RP2040 microcontroller. The Pico H has built-in USB for programming and serial communication — no external programmer required. The onboard LED is hardwired to GP25.
Pinout
Important: The GPIO number used in code (Pin(26), Pin(27)) does not correspond to the physical pin number on the board.

Link to datasheet
Environment Setup
I had already set up the environment for the XIAO RP2040 and the Pico H is the same process. Connect the Pico H to your computer via USB and transfer the .uf2 file to the controller. It will reboot and ready for me to upload MicroPython scripts directly through the IDE VS Code.
Blink Code
Exercise 1: Blink (Onboard LED)
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 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.
Input Exercises
Exercise 2: Switch (Toggle Switch)
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)
while True:
print(switch0.value())
if switch0.value() == 0:
led.value(0) # LED OFF
else:
led.value(1) # LED ON
Code Explanation
Pin(26, Pin.IN)configures GP26 as a digital input.- When the switch connects GP26 to GND, the pin reads LOW (0) and the LED turns off.
- When the switch connects GP26 to 3V3, the pin reads HIGH (1) and the LED turns on.
print(switch0.value())outputs the current switch state to the serial console, which is useful for debugging.
Wiring
The slide switch has three pins — the common (middle) pin connects to one of the two outer pins depending on the slide position.
| Connection | From | To |
|---|---|---|
| Switch Common (middle) | — | Physical pin 31 (GP26) |
| Switch outer pin (one side) | — | 3V3 |
| Switch outer pin (other side) | — | GND |
Because the switch always connects GP16 to either 3V3 or GND, the pin always has a defined state.
Pin Reference
| Code | GPIO | Physical Pin |
|---|---|---|
Pin(25) |
GP25 (onboard LED) | Internal — no pin |
Pin(26) |
GP16 | Physical pin 31 |
Exercise 3: Button (Momentary Push Button)
Read a 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)
switch0 = Pin(26, Pin.IN, Pin.PULL_UP)
while True:
print(switch0.value())
if switch0.value() == 0:
led.value(1) # LED OFF
else:
led.value(0) # LED ON

pressing the button turns the led on
Code Explanation
Pin(26, Pin.IN, Pin.PULL_UP)configures GP26 as a digital input with the internal pull-up resistor enabled.- A button only makes contact while physically pressed, so the LED turns on during the press and turns off when released.
- The logic is pressing the button connects GP26 to GND, pulling it LOW (0).
Wiring
A button has two legs (or four legs in pairs).
| Connection | From | To |
|---|---|---|
| Button leg 1 | — | Physical pin 31 (GP26) |
| 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) |
GP26 | Physical pin 31 |
Exercise 4: PWM Fading (External LED)
Same as the XIAO RP2040 exercise — fade an external LED using Pulse Width Modulation. The code is identical since both boards run the same RP2040 chip; only the physical pin locations on the board are different.
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)
Wiring
- GP26 (physical pin 31) → LED long leg (anode/positive)
- LED short leg (cathode/negative) → resistor (220Ω–470Ω) → GND
Pin Reference
| Code | GPIO | Physical Pin |
|---|---|---|
Pin(26) |
GP26 | Physical pin 31 |
Exercise 5: Bidirectional Serial Communication
Same serial command interface as the XIAO RP2040 — my Mac sends text commands over USB, the Pico H parses them and responds. The code is identical.
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)

Typed "ON"

LED turned ON
Wiring
Same external LED and button setup:
- GP26 (physical pin 31) → LED → resistor (220–470Ω) → GND
- GP27 (physical pin 32) → button leg, opposite leg → GND (internal pull-up in code)
Pin Reference
| Code | GPIO | Physical Pin |
|---|---|---|
Pin(26) |
GP26 | Physical pin 31 |
Pin(27) |
GP27 | Physical pin 32 |
Debugging: 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 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.
```
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.
What This Demonstrates
The Pico H exercises demonstrate progressively building from a basic blink to reading physical inputs, PWM output, and bidirectional serial communication — all using MicroPython. The code is identical to the XIAO RP2040 exercises since both boards share the same chip; only the physical wiring locations differ and the XIAO RP2040's onboard LED is active LOW while the Pico H's is active HIGH. Compared to the ATtiny412's single-wire UPDI toolchain, the Pico H's built-in USB makes the programming workflow significantly simpler, letting you focus on learning concepts rather than debugging the programming interface.
Reflection
What I Accomplished
I successfully programmed four microcontrollers, three 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
- Raspberry Pi Pico H — MicroPython + VS Code: LED blink, toggle switch input, and momentary button input with pull-ups, progressing from basic output to physical input reading with the simplest toolchain of the four boards
Key Takeaways
- Same concepts, different syntax. GPIO control, digital read/write, PWM, and serial communication work the same way conceptually across all four 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.
- 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.
- GPIO vs. physical pin numbering. Both the XIAO RP2040 and Pico H reinforced that the number in your code rarely matches the number printed on the board. Always cross-reference the pinout diagram.
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 | |
| Pico H | Exercise 1: LED Blink | MicroPython | picoh_blink.py |
| Pico H | Exercise 2: Slide Switch | MicroPython | picoh_switch.py) |
| Pico H | Exercise 3: Button Input | MicroPython | picoh_button.py |
| Pico H | Exercise 4: PWM Fade | MicroPython | rp2040_ex4_pwm_fade.py |
| Pico H | Exercise 5: Serial Commands | MicroPython | rp2040_ex5_serial_commands.py |
*Pico H use the same code as the RP2040 except when referencing the onboard LED.

