Skip to content

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

Link to Group 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.

  1. Downloaded the firmware — I grabbed the latest MicroPython .uf2 file from micropython.org/download/RPI_PICO.
  2. 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.
  3. Dragged and dropped — I dragged the .uf2 file 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:

  1. 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):

    cd ~/Documents/embed
    python -m venv myenv
    source myenv/bin/activate
    

  2. Installed mpremote inside the virtual environment:

    pip install mpremote
    

  3. Configured a VS Code task to automate uploading. I created a tasks.json file so that uploading code is as simple as Terminal → Run Task → "Upload to Pico". The task runs:

    source ~/Documents/embed/myenv/bin/activate && mpremote connect auto fs cp main.py : + reset
    

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 .uf2 file

XIAO RP2040 Pinout

With the USB connector facing up, I mapped out the pin layout from the board's silkscreen labels:

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

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 wiring

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

  1. The onboard LED (pin 25) doesn't fade — it only toggles between colors. I needed an external LED for PWM.
  2. PWM requires freq() to be set — without it, no signal is output even if the duty cycle is set.
  3. Resistor values matter — 1MΩ is far too high for an LED circuit. 220Ω–470Ω is the correct range for 3.3V.
  4. Test incrementally — I learned to confirm basic on/off works with Pin.OUT before adding PWM complexity.
  5. 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:

  1. Upload code using Terminal → Run Task → "Upload to Pico"
  2. Open a new terminal in VS Code and run: mpremote repl
  3. Press Ctrl+C to interrupt the running code (the >>> prompt appears)
  4. Press Ctrl+D to soft-reboot, which reruns main.py from the beginning — now print() output appears in real time
  5. 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:

  • blink command — blinks the LED 3 times using a for loop with range(3), toggling the PWM duty cycle between full on and off with half-second delays.
  • help command — reprints the available commands.
  • commands variable — I created a single variable to hold the command list string so it's used by both the startup message and the help command. This way if I add a new command, I only update it in one place.

How to Test

  1. Upload with Run Task
  2. Open a second terminal: mpremote connect auto repl
  3. Press Ctrl+C then Ctrl+D to restart cleanly
  4. 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:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

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 .py file, no compilation step, interactive REPL for testing
  • Arduino C++ — Write a .ino sketch, compiled before upload, setup() runs once and loop() runs forever

The core concepts (GPIO, digital read/write, serial output) are the same — just different syntax and workflow.


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 LOWLOW 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. webserver

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 a while(true) loop (which would freeze the board and prevent it from responding to new web requests), I used millis() to check if enough time has passed since the last toggle. This way the board stays responsive to web requests even while blinking.
  • ledStatus variable — changed from a bool to a String so it could hold three states: "ON", "OFF", or "BLINK"

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:

  1. In Arduino IDE, opened File → Preferences
  2. Added this URL to "Additional boards manager URLs":
    http://drazzy.com/package_drazzy.com_index.json
    
  3. Opened Tools → Board → Boards Manager
  4. Searched for megaTinyCore and installed it
  5. Selected Tools → Board → megaTinyCore → ATtiny412
  6. 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

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

  1. Checked wiring with multimeter — confirmed continuity on all connections between the FTDI adapter and the ATtiny412 breakout board.
  2. Tried different baud rates — switched between SerialUPDI SLOW (57600) and 230400 baud. Same error on both.
  3. Swapped TX and RX — some FTDI boards label pins from the device's perspective. Tried the resistor on RX instead of TX. Same error.
  4. 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.
  5. Stripped wiring to minimum — removed all components except VCC, GND, and the UPDI connection. Same error.
  6. Tried different resistor values — swapped 5kΩ for 1kΩ. Same error.
  7. 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.

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. The millis() 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.

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

from machine import Pin
import time

led = Pin(25, Pin.OUT)

while True:
    led.toggle()
    time.sleep(0.5)

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