10. Output Devices
This week's individual assignment is to add an output device to a microcontroller board and program it to do something. I chose to document three output devices that are directly relevant to my final project a pill dispenser, where I may use any of these to signal dispensing events, control a compartment mechanism, or provide user feedback:
- A passive buzzer (for audio feedback and melodies)
- A micro servo motor(for small actuation (lid, dispenser arm)
- A stepper motor for precise rotational control
All devices were connected to my custom PCB made on Week 08 (that works with the XIAO RP2350) and was programmed using Arduino IDE.
For full power consumption measurements, check out the group assignment.
Group Assignment.
What are Output Devices?
An output device is a component that takes an electrical signal from the microcontroller and converts it into a physical effect. It can produce movement (motor), light (LED), sound (buzzer), or an image on a screen (display). They work along with input devices to create interactive systems: the microcontroller reads data from sensors (inputs), processes it and transform it into a effect in the real world (outputs).
Understanding how much power each device consumes is critical for designing safe circuits: it determines the power supply you need, it prevents system failures or safety hazards like overheating and it helps you optimize the components efficiency in our projects.
This image represents the Ohm's Law and Joule's Law Wheel. It shows the mathematical relationships between the four primary electrical quantities.
The Four Core Variables
The inner circle identifies the four variables and the units they are measured in:
- P (Power): Measured in Watts (W) This represents the rate at which electrical energy is consumed or produced meaning how fast electricity or work is done.
- I (Current): Measured in Amps (A). The flow of electric charge through a conductor.
- V (Voltage): Measured in Volts (V). Known as electrical potential difference, it represents the electrical pressure that pushes the current.
- R (Resistance): Measured in Ohms (Ω). This is the opposition to the flow of current.
Why use this formula? Voltage and current are the easiest electrical quantities to measure directly with common instruments like multimeters and USB testers. Rather than trying to measure power or resistance directly, we simply measure volts and amps, then multiply them together to get power instantly.
Example
A servo motor running at 6 V draws 0.3 A while moving. Meaning it consumes 1.8 W
P = 6 V × 0.3 A = 1.8 W
Measuring Instruments
There are different instruments that can be used depending on the device and voltage range being tested, some of them are:
USB Tester
It measures voltage and current in real time on a small screen, so you can calculate P = V × I directly.
We connect it between the power source and the device. It has two USB ports: one to plug to the source we are powering the microcontroller with IN and connect our output device (via the costume PCB) to OUT. If you plug into IN only, it will read just the source not what the device actually consumes. It is best for 5 V USB-powered devices.
Digital Multimeter
A device that measures electrical properties: voltage, current, and resistance. There are two very different setups:
For Voltage: set the dial to V, plug the red probe into VΩ and black into COM, then touch the probes across the two points (no need to cut the circuit). For Current: set the dial to A, move the red probe to the A port, and connect the meter in series (the current must flow through it). Never connect it in parallel when measuring current it can cause a short circuit.
Clamp Ammeter
It measures the current without touching the wire. To use it, open the clamp and wrap it around one wire only (not both or they cancel out), close it, and read the current. It works by sensing the magnetic field the current creates, so there is no need to cut or disconnect anything. Best for higher power devices where direct-contact measurement would be impractical or unsafe.
Safety Notes
Before connecting any device, keep these safety rules in mind:
Understanding current levels is critical because it directly determines the danger to human safety and the stress on circuit components. ~1 mA feels tingly but is generally safe, ~10 mA can cause muscle contraction (you may not be able to let go), and ~100 mA can cause cardiac fibrillation.
Always connect the multimeter in series when measuring current
If you connect the ammeter probes in parallel (across the device like you would for voltage), you will create a near short-circuit and may damage the meter or the device. Current measurement always goes in series: the current must flow through the meter.
Microcontroller pin current limit
Most microcontroller GPIO pins can only supply 20-40 mA safely. Never connect a motor, buzzer, or LED strip directly to a GPIO pin without a driver, transistor, or MOSFET in between: these devices draw far more current than the pin can handle.
Inductive flyback on motors
Motors and solenoids are inductive loads. When power is cut, they generate a voltage spike in reverse (flyback). Always use a protection diode or a motor driver IC that includes built-in protection to avoid damaging your microcontroller.
Power Consumption Tests
What is a DC Motor?
A DC (Direct Current) motor converts electrical energy into rotational mechanical energy. When a DC voltage is applied across its terminals, the motor shaft spins. The direction of rotation depends on the polarity, and the speed depends on the voltage level. To control a DC motor with a microcontroller, an H-bridge driver IC is needed: it allows the microcontroller's low-current signals to safely switch the higher motor current, and also enables forward/reverse direction control.
Motor used: MOT-050 rated at 6V, controlled via L298N H-bridge with a potentiometer for speed control. It is powered through a 9V battery connected to a breadboard power supply module that converts it to 5V.
In this video we can see how current changes as we vary the speed.
The current was measured with the multimeter in series. The red probe was connected to the cathode (+) of the battery, and the black probe was connected to the terminal 12V of the L298N.
Power measurements
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Low speed | 5.0 | 0.065 | 0.325 W |
| Medium speed | 5.0 | 0.265 | 1.325 W |
| Full speed | 5.0 | 0.399 | 1.995 W |
Notes: The voltage was fixed at 5V from the breadboard power supply module. However if the voltage was the recommended (6V), the motor would have ran at a higher speed and power.
The multimeter read mA so we converted it to A by dividing by 1000 (1000 mA = 1 A) to get the correct Watts value.
The code controls a DC motor using a potentiometer and a button. Turning the potentiometer adjusts the motor speed (all the way left means stopped, all the way right means full speed). Pressing the button changes the direction the motor spins, but instead of stopping and reversing instantly, it slowly slows down first, pauses, then speeds back up in the opposite direction so the change is smooth and visible.
// ============================================================
// DC Motor MOT-050 — XIAO RP2350 + L298N H-Bridge
// D7 → IN1 (direction control pin 1)
// D8 → IN2 (direction control pin 2)
// D9 → ENA (PWM speed control — remove ENA jumper!)
// D1 → 5k potentiometer (speed)
// D2 → Button (direction change with gradual braking)
// ============================================================
// Pin definitions
#define PIN_IN1 D7
#define PIN_IN2 D8
#define PIN_ENA D9
#define PIN_POT D1
#define PIN_BTN D2
// true = forward, false = backward
bool direction = true;
bool changing = false;
int currentSpeed = 0; // current PWM value (0-255)
// Debounce variables — prevent false button triggers from mechanical noise
bool btnState = HIGH;
bool btnStatePrev = HIGH;
unsigned long debounceTime = 0;
#define DEBOUNCE_MS 50 // ms to wait until button signal is stable
// ── detectClick() ────────────────────────────────────────────
// Returns true once when button is pressed.
// Debounce: ignores signal until it stays stable for DEBOUNCE_MS.
bool detectClick() {
bool reading = digitalRead(PIN_BTN);
if (reading != btnStatePrev) debounceTime = millis(); // reset timer on change
btnStatePrev = reading;
if ((millis() - debounceTime) >= DEBOUNCE_MS) {
if (reading != btnState) {
btnState = reading;
if (btnState == LOW) return true; // LOW = pressed (internal pull-up)
}
}
return false;
}
// ── setMotor(dir, vel) ───────────────────────────────────────
// Sends direction + speed to L298N.
// Forward: IN1=HIGH, IN2=LOW
// Backward: IN1=LOW, IN2=HIGH
// Speed: PWM 0-255 on ENA (0=stop, 255=full speed)
void setMotor(bool dir, int vel) {
if (vel <= 0) {
digitalWrite(PIN_IN1, LOW);
digitalWrite(PIN_IN2, LOW);
analogWrite(PIN_ENA, 0);
} else {
if (dir) {
digitalWrite(PIN_IN1, HIGH); // forward
digitalWrite(PIN_IN2, LOW);
} else {
digitalWrite(PIN_IN1, LOW); // backward
digitalWrite(PIN_IN2, HIGH);
}
analogWrite(PIN_ENA, vel);
}
}
// ── brakeAndSwitch() ─────────────────────────────────────────
// Called on button press:
// 1. Ramps speed DOWN from currentSpeed → 0 (step -8 every 20ms)
// 2. Pauses 300ms at zero
// 3. Flips direction
// 4. Ramps speed UP from 0 → currentSpeed (step +8 every 20ms)
// Adjust step size and delay(20) to change smoothness.
void brakeAndSwitch() {
Serial.println("Braking...");
// Ramp down
for (int v = currentSpeed; v >= 0; v -= 8) {
setMotor(direction, v);
delay(20);
}
setMotor(direction, 0);
delay(300); // brief pause at zero
// Switch direction
direction = !direction;
Serial.println(direction ? ">>> FORWARD" : ">>> BACKWARD");
// Ramp up
Serial.println("Accelerating...");
for (int v = 0; v <= currentSpeed; v += 8) {
setMotor(direction, v);
delay(20);
}
setMotor(direction, currentSpeed);
}
// ── setup() ──────────────────────────────────────────────────
// Runs once at boot. Sets up pins and starts Serial.
void setup() {
Serial.begin(115200);
// RP2350 has 12-bit ADC natively → values 0-4095 (not 0-1023)
analogReadResolution(12);
pinMode(PIN_IN1, OUTPUT);
pinMode(PIN_IN2, OUTPUT);
pinMode(PIN_ENA, OUTPUT);
// Internal pull-up: unpressed = HIGH, pressed = LOW
pinMode(PIN_BTN, INPUT_PULLUP);
// Start with motor stopped
digitalWrite(PIN_IN1, LOW);
digitalWrite(PIN_IN2, LOW);
analogWrite(PIN_ENA, 0);
Serial.println("Ready. Turn pot for speed, press button to switch direction.");
}
// ── loop() ───────────────────────────────────────────────────
// Runs continuously. Reads pot, checks button, drives motor.
void loop() {
// Read pot (0-4095) → map to PWM (0-255)
// (raw * 255) / 4095 scales 12-bit ADC to 8-bit PWM
uint32_t raw = analogRead(PIN_POT);
currentSpeed = (int)((raw * 255UL) / 4095);
// Button pressed → brake and switch direction
if (detectClick()) {
brakeAndSwitch();
}
// Dead zone: below 15 PWM the motor just vibrates without spinning
// so we cut it off completely to avoid wasted current and noise
if (currentSpeed < 15) {
setMotor(direction, 0);
} else {
setMotor(direction, currentSpeed);
}
delay(50); // small delay to avoid flooding ADC reads
}
What is a Servo Motor?
A servo motor is a motor with a built-in position feedback system. Unlike the DC motor that just spins, a servo receives a PWM (Pulse Width Modulation) signal that tells it to move to a specific angle, typically between 0° and 180°. It continuously adjusts its position to match the commanded angle, making it ideal for precise control applications. Servos usually have three wires: power (5-6 V), ground, and signal (PWM from the microcontroller).
Servos used: Micro servo sg90 & MG995 Servo
To measure the current drawn by the micro servo, we used a multimeter connected in series.
Power measurements
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Average | 5.0 | 0.100 | 0.5 W |
Notes: The voltage 5V is fixed from the microcontroller pin and the current changes because it depends on the speed angle and mechanical load.
The second servo (MG995) current was measured with a USB tester.
Power measurements
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Average | 5.0 | 0.350 | 1.75 W |
Notes: This servo has a higher current draw and torque compared to the micro servo.
Also the USB tester provided a more accurate reading of the current of all the system.
For both servos it was used the same code. It reads the potentiometer position and converts it into an angle between 0 and 180 degrees, then sends that angle to the servo. As you turn the knob, the servo follows in real time.
#include // Library that handles the PWM signal the servo needs
// Pin definitions
const int PIN_POT = D1; // Potentiometer wiper connected to D1 (analog input)
const int PIN_SERVO = D10; // Servo signal wire connected to D10 (PWM output)
Servo servo; // Create a Servo object to control the motor
void setup() {
Serial.begin(115200); // Start Serial Monitor at 115200 baud for debugging
servo.attach(PIN_SERVO); // Tell the library which pin the servo is on
}
void loop() {
// Read the potentiometer — returns a value between 0 and 1023
// 0 = knob fully left, 1023 = knob fully right
int adcValue = analogRead(PIN_POT);
// Map the potentiometer range (0-1023) to servo angle range (0-180 degrees)
// map(value, fromLow, fromHigh, toLow, toHigh)
// Example: adcValue=512 → angle=90 (center position)
int angle = map(adcValue, 0, 1023, 0, 180);
// Send the angle to the servo
// The Servo library converts this to the correct PWM pulse width automatically
servo.write(angle);
// Print both values to Serial Monitor so you can see what's happening
Serial.print("ADC: ");
Serial.print(adcValue);
Serial.print(" -> Angle: ");
Serial.println(angle);
delay(20); // Wait 20ms between updates — servos need time to reach position
}
What is a Stepper Motor?
A stepper motor moves in discrete steps rather than continuously spinning. Each electrical pulse sent to the driver moves the shaft by a fixed angle, typically 1.8° per step, meaning 200 steps per full revolution. This makes stepper motors ideal for precise position control without needing an encoder, which is why they are used in 3D printers and CNC machines. Stepper motors have 4 wires forming two independent coils (A and B); the color-coded pairs must be connected to the driver's A1/A2 and B1/B2 outputs. They also require a dedicated driver IC (like the A4988 or DRV8428) that handles the step/direction signals and controls the current through the motor coils. Stepper motors consume current even when holding position (holding torque), which is different from DC motors.
Stepper used: NEMA 17hs4401.
Driver: Drv8825.
The test shows the speed changes (slow/fast) and direction reversal trough two potentiometers (direction and speed)
POWER MEASUREMENTS
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Off (driver disabled) | 12.0 | 0.0 | 0.0 W |
| Slow speed | 12.0 | 0.550 | 6.60 W |
| Fast speed | 12.0 | 0.250 | 3.00 W |
Notes: Stepper motors draw current even while holding position, unlike DC or servo motors. Disabling the driver when holding is not needed reduces heat and power waste.
Stepper motors draw more current at lower speeds due to back-EMF (back electromotive force). When the motor spins fast, it generates an internal voltage that opposes the incoming current, naturally limiting consumption.
This code controls a stepper motor with a button and two potentiometers: the button turns the motor on and off (and when off, it fully disables the driver so it doesn't make noise or overheat), one potentiometer controls how fast it spins by adjusting the time between each step, and the other controls which direction it spins depending on whether it's above or below the rage midpoint.
// NEMA 17HS4401 Stepper Motor – Speed & Direction Test// ── Pin definitions ──────────────────────────────────────────────────────────
#define STEP_PIN D5 // Step pulses to DRV8825
#define DIR_PIN D6 // Direction control
#define EN_PIN D7 // Enable pin — LOW = active, HIGH = driver disabled (quiet/cool)
#define POT_VEL D0 // Speed potentiometer
#define POT_DIR D1 // Direction potentiometer
#define BOTON D2 // Toggle button
bool motorOn = false; // Motor state
bool prevState = HIGH; // Previous button reading (for edge detection)
void setup() {
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(EN_PIN, OUTPUT);
pinMode(BOTON, INPUT_PULLUP);
digitalWrite(EN_PIN, HIGH); // Start disabled — no noise, no heat
}
void loop() {
// ── Button toggle (detects press edge only) ─────────────────────────────
bool currState = digitalRead(BOTON);
if (prevState == HIGH && currState == LOW) { // Falling edge = button pressed
motorOn = !motorOn;
digitalWrite(EN_PIN, motorOn ? LOW : HIGH); // LOW = enable, HIGH = disable
delay(50); // Debounce
}
prevState = currState;
// ── Motor running ───────────────────────────────────────────────────────
if (motorOn) {
// Average 3 ADC readings to reduce noise
int sum = 0;
for (int i = 0; i < 3; i++) sum += analogRead(POT_VEL);
int velVal = sum / 3;
int dirVal = analogRead(POT_DIR);
// Direction: above midpoint = CW, below = CCW
digitalWrite(DIR_PIN, dirVal > 512 ? HIGH : LOW);
// Map pot value to step delay (higher pot = smaller delay = faster)
int speed = map(velVal, 50, 973, 6000, 400);
speed = constrain(speed, 400, 6000);
// Generate one step pulse
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(speed);
digitalWrite(STEP_PIN, LOW);
delayMicroseconds(speed);
} else {
digitalWrite(STEP_PIN, LOW); // Ensure STEP stays low when off
}
}
What is a Neopixel Ring?
A Neopixel ring is a circular, rigid printed circuit board (PCB) featuring a chain of individually addressable, super-bright RGB or RGBW LEDs. They utilize WS2812B or similar driver chips integrated directly into the LEDs, allowing all pixels to be controlled via a single digital input pin.
LED ring used: WS2812B NeoPixel ring, connected to the custom PCB via 5 V, GND and Data (GPIO).
The NeoPixel ring has three connections: 5 V power, GND, and Data (to a digital GPIO pin). A 300–500 Ω resistor on the data line is recommended to prevent signal reflection damage. Here we connected it directly to the custom PCB.
Setup: USB tester connected between the 5 V power source and the ring to measure voltage and current in real time while running different color patterns.
POWER MEASUREMENTS
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| All LEDs off | 4.990 | 0.000 | 0.000 W |
| All LEDs on – lowest intensity | 4.986 | 0.000 | 0.000 W |
| All LEDs on – medium intensity | 4.937 | 0.170 | 0.839 W |
| All LEDs on – highest intensity | 4.937 | 0.247 | 1.219 W |
| All LEDs red (full) | 4.960 | 0.208 | 1.031 W |
| All LEDs purple (full) | 4.938 | 0.214 | 1.056 W |
| All LEDs pink (full) | 4.941 | 0.336 | 1.660 W |
| All LEDs blue (full) | 4.963 | 0.311 | 1.543 W |
| All LEDs cyan (full) | 4.927 | 0.326 | 1.606 W |
| All LEDs green (full) | 4.957 | 0.203 | 1.006 W |
| All LEDs yellow (full) | 4.930 | 0.305 | 1.503 W |
Notes: Single-channel colours (red, green) draw less current than
two-channel ones (cyan, yellow). If we added the white color (which uses all three channels) it would draw even more current.
Even at "lowest intensity" the USB tester showed 0.000 A, meaning consumption is
below its measurement resolution.
This code controls a 16-LED NeoPixel ring using two potentiometers: one selects the base color by sweeping through the full HSV color wheel, and the other controls how fast a rainbow gradient spins around the ring. When pressing the button, the led mode changes, now the potentiometers control brightness and color hue in all neopixels.
// NeoPixel Ring#include
#define PIN_NEO D10
#define NUM_LEDS 16
#define PIN_COLOR D0
#define PIN_VEL D1
Adafruit_NeoPixel strip(NUM_LEDS, PIN_NEO, NEO_GRB + NEO_KHZ800);
// NEO_GRB = color order of WS2812B chip; NEO_KHZ800 = 800 kHz data signal
uint16_t hueBase = 0;
// Increases every loop to create the spinning animation; wraps 65535→0 automatically
void setup() {
analogReadResolution(12);
// RP2350 has a native 12-bit ADC → values 0–4095 instead of the usual 0–1023
strip.begin();
strip.setBrightness(60);
// ~24% brightness (60/255) — keeps current consumption safe for USB power
strip.clear();
strip.show();
}
void loop() {
uint32_t rawColor = analogRead(PIN_COLOR);
uint16_t colorBase = (uint16_t)((rawColor * 65535UL) / 4095);
// Scales 0–4095 to 0–65535 to cover the full HSV hue wheel
// UL suffix (unsigned long) prevents integer overflow during multiplication
uint32_t rawVel = analogRead(PIN_VEL);
int speed = map(rawVel, 0, 4095, 3, 60);
// Low pot → 3 ms delay (fast spin) | High pot → 60 ms delay (slow spin)
for (int i = 0; i < NUM_LEDS; i++) {
uint16_t hue = colorBase + hueBase + (i * 65536UL / NUM_LEDS);
// Spreads LEDs evenly across the hue wheel, shifted by pot + spin offset
uint32_t color = strip.gamma32(strip.ColorHSV(hue, 255, 200));
// ColorHSV(hue, saturation, brightness) — 255 = pure color, 200 = ~78% bright
// gamma32() corrects for the eye's logarithmic perception of brightness
strip.setPixelColor(i, color);
}
strip.show();
hueBase += 256;
// Advances the gradient by ~0.4% of the color wheel per frame → continuous spin
delay(speed);
}
What is an OLED Display?
An OLED (Organic Light-Emitting Diode) display emits its own light: each pixel is an individual LED, so no backlight is needed. This makes OLEDs thinner, more power-efficient, and capable of true black (pixels simply off). The common SSD1306 0.96" OLED communicates over I2C or SPI and is powered at 3.3 V. Power consumption depends directly on how many pixels are lit: a mostly-black screen uses almost no power.
Display tested: SSD1306 0.96" OLED – I2C. Connected to the custom PCB via I2C at 3.3 V.
For the OLED display we connected it to the custom PCB using the next connections from the OLED the GND goes to the PCB’s GND, the SDA to the PCB’s D4, the SCK to the PCB’s D5 and VCC to 3.3V.
POWER MEASUREMENTS
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| All pixels off | 3.3 | 0.0 | 0.0 W |
| Showing text | 4.920 | 0.075 | 0.369 W |
Notes: These measurements include the total PCB system current, so the display alone draws even less.
We used the U8g2 library to test the display. The code below was used for the text.
// OLED SSD1306 – Power test with U8g2 library
#include <U8g2lib.h>
#include <Wire.h>
// Hardware I2C, full frame buffer (F), no reset pin
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
void setup() {
u8g2.begin();
// ── All pixels WHITE (max current draw) ──────────────────
u8g2.clearBuffer();
u8g2.setDrawColor(1);
u8g2.drawBox(0, 0, 128, 64); // Fill entire screen
u8g2.sendBuffer();
delay(3000);
// ── Showing text (realistic use) ─────────────────────────
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 20, "Hola!");
u8g2.sendBuffer();
delay(3000);
// ── All pixels OFF (min current draw) ────────────────────
u8g2.clearBuffer();
u8g2.sendBuffer();
}
void loop() {} // Single-shot measurement — no loop needed
What is a Passive Buzzer?
A passive buzzer contains a piezoelectric element that vibrates when driven by an alternating signal. Unlike an active buzzer (which has a built-in oscillator and only needs power), a passive buzzer requires a PWM or tone signal from the microcontroller to produce sound (the frequency of the signal determines the pitch of the note). This makes it ideal for playing melodies. It has three pins: Signal (PWM), VCC+, and GND.
Buzzer connected to the custom PCB: Signal to GPIO 26 (D0), VCC to 3.3 V.
For this test we used a passive buzzer, which we connected to the custom PCB. The buzzer has 3 connections, which are Signal, VCC+ and GND. Those have to be connected to the custom PCB, first the GND goes to the GND, Signal goes to the GPIO 26 (D0) and the VCC+ goes to the 3.3V.
Here we have the comparison between the buzzer not working and working. While we moved the potentiometers we were able to notice that it changed an amp per note approximately.
Idle= V=
Active= V= 4.992 A= 0.020-0.033 using the highest value= 4.992x0.033= 0.1647
POWER MEASUREMENTS
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Idle (no tone) | 5.000 | 0.000 | 0.000 |
| Active 1 (tone playing) | 4.987 | 0.023 | 0.115 W |
| Active 2 (tone playing) | 4.999 | 0.030 | 0.150 W |
| Active 3 (tone playing) | 5.000 | 0.026 | 0.130 W |
| Active average | 4.995 | 0.026 | 0.130 W |
We used a note scale and two potentiometers to step through different pitches in real time. Each potentiometer position selects a different note, letting us observe how current changes with frequency.
// Passive Buzzer – Note scale with potentiometer control
const int PIN_BUZZER = D4;
const int PIN_TONO = D1;
// Musical scale: C D E F G A B C
float notes[] = {261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25};
int totalNotes = 8;
void setup() {
pinMode(PIN_BUZZER, OUTPUT);
}
void loop() {
static int counter = 0;
static float currentNote = 261.63;
// Read pot every 100 cycles to avoid interrupting the tone
if (counter == 0) {
int sum = 0;
for (int i = 0; i < 3; i++) sum += analogRead(PIN_TONO);
int potValue = sum / 3;
// Map pot value to one of the 8 notes
int index = constrain(map(potValue, 10, 1013, 0, totalNotes - 1), 0, totalNotes - 1);
currentNote = notes[index];
}
counter++;
if (counter >= 100) counter = 0;
// Generate one wave cycle manually (tone() doesn't work well on RP2350)
long periodUs = 1000000L / currentNote;
long halfUs = periodUs / 2;
digitalWrite(PIN_BUZZER, HIGH);
delayMicroseconds(halfUs);
digitalWrite(PIN_BUZZER, LOW);
delayMicroseconds(halfUs);
}
We also tried a melody code with help of Claude. The prompt was "transform the following notes from the song "Take on me" for a passive buzzer considering my PCB pin out."
const int PIN_BUZZER = D4;
// Note frequencies (Hz)
#define NOTE_B4 493.88
#define NOTE_D5 587.33
#define NOTE_E5 659.25
#define NOTE_FS5 739.99
#define NOTE_GS5 830.61
#define NOTE_A5 880.00
#define NOTE_B5 987.77
// Melody - Take On Me (A-ha)
float melody[] = {
NOTE_FS5, NOTE_FS5, NOTE_D5, NOTE_B4, NOTE_B4, NOTE_E5,
NOTE_E5, NOTE_E5, NOTE_GS5, NOTE_GS5, NOTE_A5, NOTE_B5,
NOTE_A5, NOTE_A5, NOTE_A5, NOTE_E5, NOTE_D5, NOTE_FS5,
NOTE_FS5, NOTE_FS5, NOTE_E5, NOTE_E5, NOTE_FS5, NOTE_E5
};
// Note durations: 8 = eighth note, 4 = quarter note, 5 = dotted quarter
int durations[] = {
8, 8, 8, 4, 4, 4,
4, 5, 8, 8, 8, 8,
8, 8, 8, 4, 4, 4,
4, 5, 8, 8, 8, 8
};
// Auto-calculate song length from array size
int songLength = sizeof(melody) / sizeof(melody[0]);
// Manual tone generator - replaces tone() which doesn't work on RP2350
// Calculates period from frequency and toggles pin for the given duration
void playTone(float frequency, int durationMs) {
if (frequency == 0) {
delay(durationMs); // silence
return;
}
long periodUs = 1000000L / frequency; // period in microseconds
long halfUs = periodUs / 2; // 50% duty cycle = best tone
long cycles = (durationMs * 1000L) / periodUs;
for (long i = 0; i < cycles; i++) {
digitalWrite(PIN_BUZZER, HIGH);
delayMicroseconds(halfUs);
digitalWrite(PIN_BUZZER, LOW);
delayMicroseconds(halfUs);
}
}
void setup() {
pinMode(PIN_BUZZER, OUTPUT);
}
void loop() {
for (int i = 0; i < songLength; i++) {
int duration = 1000 / durations[i]; // convert note value to milliseconds
playTone(melody[i], duration);
delay(duration * 1.3); // short pause between notes (same as original)
}
delay(2000); // pause before repeating
}
Here is the demonstration of the buzzer playing the "Take on Me" melody:
When we used the USB tester it didn't detect current in some notes, however when it did they were 0.020 A
POWER MEASUREMENT
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Average | 4.990 | 0.020 | 0.099 W |
The passive buzzer I used was from the 37-arduino sensor kit and it was connected directly to the XIAO RP2350, trough three pins: S (signal/PWM), VCC, and GND. I chose a passive buzzer instead of an active one because I can play with the tone which is useful for the pill dispenser project to signal different events with different alarm melodies. I didn't use a transistor, however after the group assignment I found that the buzzer draws around 30 mA at 5 V, which is within the safe limits of the XIAO pin (40 mA max). So for future reference, I will add a 100 µF capacitor across VCC and GND to smooth out any voltage spikes when the buzzer is active.
Pin connections
| Buzzer pin | XIAO RP2350 pin |
|---|---|
| S (signal) | D4 |
| VCC (+) | 3.3V |
| GND (−) | GND |
tone() function does not work reliably on the RP2350 core, so all audio was generated
manually by toggling the pin at the desired frequency.
The two potentiometers from the kit were used to control note selection (5 kΩ on D1) and were later repurposed to control melody playback speed once the songs were programmed.
How I developed the code in Arduino IDE
Step 1 — Why not use tone()?
My first attempt used the standard Arduino tone() function. After uploading,
the buzzer produced no sound at all. Searching the Seeed Studio forums confirmed the issue:
the RP2350 Arduino core does not fully support tone() on all GPIO pins.
The solution was to generate the PWM wave manually using
digitalWrite() and delayMicroseconds():
set the pin HIGH for half the wave period, then LOW for the other half.
This gives a 50% duty cycle, which is the optimal point for passive buzzers.
// Manual tone — works on RP2350 where tone() doesn't
void playTone(float frequency, int durationMs) {
long periodUs = 1000000L / frequency; // period in µs
long halfUs = periodUs / 2; // 50% duty cycle
long cycles = (durationMs * 1000L) / periodUs;
for (long i = 0; i < cycles; i++) {
digitalWrite(PIN_BUZZER, HIGH);
delayMicroseconds(halfUs);
digitalWrite(PIN_BUZZER, LOW);
delayMicroseconds(halfUs);
}
}
Step 2 — Stable note selection with a potentiometer
I mapped the 5 kΩ potentiometer to an 8-note musical scale (Do–Re–Mi–Fa–Sol–La–Si–Do). The first version read the pot every loop cycle, which caused the tone to break and stutter. The fix: read the pot only every 100 wave cycles, so the tone plays uninterrupted.
// Do Re Mi Fa Sol La Si Do (C4 to C5)
float notes[] = {261.63, 293.66, 329.63, 349.23,
392.00, 440.00, 493.88, 523.25};
void loop() {
static int counter = 0;
static float currentNote = 261.63;
// Read pot every 100 cycles — avoids interrupting the tone
if (counter == 0) {
int sum = 0;
for (int i = 0; i < 3; i++) sum += analogRead(PIN_TONO);
int val = sum / 3;
int idx = constrain(map(val, 10, 1013, 0, 7), 0, 7);
currentNote = notes[idx];
}
counter++;
if (counter >= 100) counter = 0;
long periodUs = 1000000L / currentNote;
long halfUs = periodUs / 2;
digitalWrite(PIN_BUZZER, HIGH);
delayMicroseconds(halfUs);
digitalWrite(PIN_BUZZER, LOW);
delayMicroseconds(halfUs);
}
Step 3 — Playing "Take On Me" (A-ha)
I found a community Arduino sketch by GeneralSpud that uses the standard
pitches.h library and tone(). Since those don't work on
the RP2350, I adapted it: replaced pitches.h with direct frequency
#defines, and replaced tone() with my playTone()
function. The note durations and structure remained unchanged.
const int PIN_BUZZER = D4;
// Note frequencies (Hz)
#define NOTE_B4 493.88
#define NOTE_D5 587.33
#define NOTE_E5 659.25
#define NOTE_FS5 739.99
#define NOTE_GS5 830.61
#define NOTE_A5 880.00
#define NOTE_B5 987.77
// Take On Me — A-ha
float melody[] = {
NOTE_FS5, NOTE_FS5, NOTE_D5, NOTE_B4, NOTE_B4, NOTE_E5,
NOTE_E5, NOTE_E5, NOTE_GS5, NOTE_GS5, NOTE_A5, NOTE_B5,
NOTE_A5, NOTE_A5, NOTE_A5, NOTE_E5, NOTE_D5, NOTE_FS5,
NOTE_FS5, NOTE_FS5, NOTE_E5, NOTE_E5, NOTE_FS5, NOTE_E5
};
// Note durations: 8=eighth, 4=quarter, 5=dotted quarter
int durations[] = {
8,8,8,4,4,4, 4,5,8,8,8,8, 8,8,8,4,4,4, 4,5,8,8,8,8
};
int songLength = sizeof(melody) / sizeof(melody[0]);
void playTone(float frequency, int durationMs) {
if (frequency == 0) { delay(durationMs); return; }
long periodUs = 1000000L / frequency;
long halfUs = periodUs / 2;
long cycles = (durationMs * 1000L) / periodUs;
for (long i = 0; i < cycles; i++) {
digitalWrite(PIN_BUZZER, HIGH);
delayMicroseconds(halfUs);
digitalWrite(PIN_BUZZER, LOW);
delayMicroseconds(halfUs);
}
}
void setup() { pinMode(PIN_BUZZER, OUTPUT); }
void loop() {
for (int i = 0; i < songLength; i++) {
int dur = 1000 / durations[i]; // note value → milliseconds
playTone(melody[i], dur);
delay(dur * 1.3); // gap between notes
}
delay(2000); // pause before repeat
}
Here is a demo of the buzzer playing the musical scale controlled by the potentiometer, followed by the "Take On Me" melody:
What I learned
The biggest challenge was discovering that tone() doesn't work on the RP2350.
Generating the waveform manually taught me how frequency and duty cycle actually produce sound —
the 50% duty cycle gives the cleanest tone on a passive buzzer. I also learned that reading
analog inputs inside a tight audio loop breaks the waveform; batching reads every N cycles
is the solution.
What is a Micro Servo?
A servo motor uses PWM to move its shaft to a precise angle (0–180°). The signal wire receives a pulse every 20 ms; the pulse width determines the angle: ~1 ms = 0°, ~1.5 ms = 90°, ~2 ms = 180°. Servos are ideal for the pill dispenser project because they can precisely open and close a compartment lid.
The micro servo has three wires. It was powered from the 5 V rail of the breadboard power supply (not from the XIAO, which only provides 3.3 V on its pins and cannot source enough current for the servo). The signal wire connects to any PWM-capable GPIO.
Pin connections
| Servo wire | Color (typical) | Connected to |
|---|---|---|
| Signal | Orange / Yellow | XIAO D10 |
| VCC | Red | 5 V (power supply rail) |
| GND | Brown / Black | GND (common) |
The 50 kΩ potentiometer (D0) was wired with its wiper to the analog pin and its two ends to 3.3 V and GND to control the servo angle in real time.
How I developed the code in Arduino IDE
Step 1 — Installing the Servo library
In Arduino IDE, I opened Sketch → Include Library → Manage Libraries
and searched for "Servo". I installed the Servo library
by Michael Margolis / Arduino. This library handles all PWM timing automatically —
I just call servo.write(angle) with a value from 0 to 180.
After installing, I tested with a minimal sketch: attach the servo to pin D10 and write angle 90 to confirm it centers correctly.
#include <Servo.h>
Servo servo;
void setup() {
servo.attach(D10); // attach servo to pin D10
servo.write(90); // move to center position
}
void loop() {}
Step 2 — Potentiometer control
I connected the 50 kΩ pot to D0 and mapped its 0–1023 range to 0–180°.
Turning the knob moves the servo shaft in real time. The
map() function handles the conversion.
#include <Servo.h>
const int PIN_POT = D0; // 50K potentiometer
const int PIN_SERVO = D10; // servo signal
Servo servo;
void setup() {
servo.attach(PIN_SERVO);
}
void loop() {
int adcValue = analogRead(PIN_POT);
// Map 0-1023 → 0-180 degrees
int angle = map(adcValue, 0, 1023, 0, 180);
servo.write(angle);
delay(20); // servos need ~20ms to reach position
}
Step 3 — Button-triggered positions (pill dispenser logic)
For the final project context, I also tested controlling the servo with a button
to cycle through three fixed positions (0°, 90°, 180°). This mimics how a
dispenser lid might open and close. The button uses INPUT_PULLUP so
no external resistor is needed.
#include <Servo.h>
const int PIN_BTN = D2;
const int PIN_SERVO = D10;
Servo servo;
int position = 0; // 0, 1, or 2
bool prevBtn = HIGH;
int angles[] = {0, 90, 180}; // three dispensing positions
void setup() {
pinMode(PIN_BTN, INPUT_PULLUP);
servo.attach(PIN_SERVO);
servo.write(angles[0]);
}
void loop() {
bool currBtn = digitalRead(PIN_BTN);
// Detect falling edge (button press)
if (prevBtn == HIGH && currBtn == LOW) {
position = (position + 1) % 3; // cycle 0 → 1 → 2 → 0
servo.write(angles[position]);
delay(50); // debounce
}
prevBtn = currBtn;
}
The video below shows the micro servo responding to the potentiometer in real time:
What I learned
The most important lesson was powering the servo from an external 5 V rail rather than from the XIAO. When I tried powering it from 3.3 V the servo twitched but couldn't hold a position. Once moved to the 5 V rail it worked perfectly. For the pill dispenser, the button-triggered version is more practical than the pot version — it gives clean discrete positions for each compartment.
What is a Stepper Motor?
A stepper motor moves in discrete steps (typically 1.8° per step = 200 steps/revolution). Each pulse on the STEP pin advances the shaft by one step; the DIR pin sets the direction. Unlike DC motors, steppers hold position precisely without an encoder, making them ideal for mechanisms that need exact rotational control — like rotating a pill carousel. They require a dedicated driver IC; I used the DRV8825.
The NEMA 17 stepper (Oukeda 17HS4401, 12 V, 1.7 A) was driven by a DRV8825 module on a breadboard. The motor coils were identified with a multimeter before wiring: pairs that showed ~4 Ω resistance are the same coil.
DRV8825 wiring
| DRV8825 pin | Connected to | Notes |
|---|---|---|
| VMOT | 12 V (power supply +) | Motor power |
| GND (motor side) | Power supply − | Motor ground |
| GND (logic side) | XIAO GND | Common ground |
| RST + SLP | XIAO 3.3 V (joined) | Must be HIGH to enable |
| EN | XIAO D7 | LOW = active, HIGH = silent off |
| STEP | XIAO D5 | One pulse = one step |
| DIR | XIAO D6 | HIGH / LOW = direction |
| M0, M1, M2 | GND | Full-step mode (200 steps/rev) |
Motor coil wiring (17HS4401)
| Wire color | DRV8825 pin |
|---|---|
| Red | 2A |
| Blue | 1A |
| Green | 1B |
| Black | 2B |
How I developed the code in Arduino IDE
Step 1 — No library needed
Unlike the servo, the DRV8825 only needs two signals: STEP pulses and DIR level.
No library is required — just digitalWrite() and
delayMicroseconds(). Each HIGH/LOW pair on STEP = one motor step.
The delay between pulses controls speed: shorter delay = faster rotation.
#define STEP_PIN D5
#define DIR_PIN D6
void setup() {
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
digitalWrite(DIR_PIN, HIGH); // clockwise
}
void loop() {
// One step
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(1000); // 1 ms half-period
digitalWrite(STEP_PIN, LOW);
delayMicroseconds(1000);
// 500 µs → fast, 5000 µs → slow
}
Step 2 — Adding EN pin to eliminate holding noise
When the motor was stopped, the driver still energized the coils, producing a faint buzzing noise and wasting power. Connecting EN to GPIO D7 and setting it HIGH when stopped completely cuts power to the coils — no noise, no heat.
Key insight: EN on the DRV8825 is active LOW — LOW enables the driver, HIGH disables it. This is the opposite of what feels intuitive.
#define EN_PIN D7
void setup() {
pinMode(EN_PIN, OUTPUT);
digitalWrite(EN_PIN, HIGH); // start disabled = silent
}
// When button pressed:
// motorOn → digitalWrite(EN_PIN, LOW) → driver active
// motorOff → digitalWrite(EN_PIN, HIGH) → silent, cool
Step 3 — Final code: button + two potentiometers
The final version uses a button (D2) to toggle the motor on/off, the 50 kΩ pot (D0) for speed, and the 5 kΩ pot (D1) for direction. Reading the pots every loop cycle was destabilizing the step timing, so I averaged 3 readings with a short delay to smooth the ADC noise.
// NEMA 17HS4401 + DRV8825 — XIAO RP2350
#define STEP_PIN D5
#define DIR_PIN D6
#define EN_PIN D7 // LOW = active, HIGH = silent off
#define POT_VEL D0 // 50K — speed
#define POT_DIR D1 // 5K — direction
#define BOTON D2 // toggle on/off
bool motorOn = false;
bool prevState = HIGH;
void setup() {
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(EN_PIN, OUTPUT);
pinMode(BOTON, INPUT_PULLUP);
digitalWrite(EN_PIN, HIGH); // start silent
}
void loop() {
bool currState = digitalRead(BOTON);
if (prevState == HIGH && currState == LOW) {
motorOn = !motorOn;
digitalWrite(EN_PIN, motorOn ? LOW : HIGH);
delay(50); // debounce
}
prevState = currState;
if (motorOn) {
// Average 3 ADC reads to reduce noise
int sum = 0;
for (int i = 0; i < 3; i++) sum += analogRead(POT_VEL);
int velVal = sum / 3;
int dirVal = analogRead(POT_DIR);
// Direction: above midpoint = CW
digitalWrite(DIR_PIN, dirVal > 512 ? HIGH : LOW);
// Speed: larger delay = slower. Range 400–6000 µs
int speed = map(velVal, 50, 973, 6000, 400);
speed = constrain(speed, 400, 6000);
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(speed);
digitalWrite(STEP_PIN, LOW);
delayMicroseconds(speed);
} else {
digitalWrite(STEP_PIN, LOW);
}
}
The video below shows the stepper motor running at different speeds and directions, toggled with the button:
Power measurements
| State | Voltage (V) | Current (A) | Power (W) |
|---|---|---|---|
| Driver disabled (EN HIGH) | 12.0 | 0.0 | 0.0 W |
| Slow speed | 12.0 | 0.550 | 6.60 W |
| Fast speed | 12.0 | 0.250 | 3.00 W |
Note: Steppers draw more current at low speeds due to back-EMF — the motor generates an opposing voltage when spinning fast, which naturally limits current.
What I learned
The most critical steps were the Vref calibration before connecting the motor, and discovering the EN pin polarity (active LOW). Skipping Vref can destroy the motor or driver. Using the EN pin properly completely eliminated the holding-torque buzz and reduced heat significantly. For the pill dispenser, the stepper is likely too large (NEMA 17), but the same DRV8825 code would work with a smaller NEMA 14 or even a 28BYJ-48.
Reflections
Working with three different output devices in the same week gave me a clear picture of the trade-offs for my pill dispenser project:
-
Passive buzzer — perfect for audio alerts when it's time to take a pill.
Extremely low power (≈0.1 W), connects directly to GPIO, and can play distinct tones per alarm.
The manual PWM approach is essential on the RP2350 since
tone()is unreliable. - Micro servo — best fit for the dispenser mechanism due to its compact size and precise angular control. Needs 5 V external power and a common GND with the microcontroller.
- NEMA 17 stepper — too large for the final product, but the DRV8825 driver and code logic (STEP/DIR/EN) will be directly reusable with a smaller stepper if needed. The Vref calibration procedure is the same regardless of motor size.