Skip to content
All work

Embedded systems & robotics2026

Pico 2 Robot with PID Speed Control

A two-wheel robot on a Raspberry Pi Pico 2, written in C, that holds each wheel's speed with encoder feedback and PID, and drove a straight 6 second run with its wheels 2 pulses apart.

Select any part of the robot to see what it does.

A test run, animated in your browser. The pulse counts are from the lab report; the speed curves are drawn to show how the loop behaves, not recorded.

Role
Firmware, wiring and testing
Timeline
Fall 2026, CSE325 at ASU
Team
Solo or team size
Stack
  • C,
  • Raspberry Pi Pico SDK 2.3.1,
  • RP2350 (Arm Cortex-M33),
  • PWM and GPIO interrupts,
  • CMake,
  • VS Code
Links

    Overview

    A two-wheel robot for CSE325 at ASU, built on a Raspberry Pi Pico 2 with two DC motors on a dual H-bridge, and grown in phases. In Phase 1, each flip of a switch makes the robot drive one smooth circle to the left or right, while the firmware prints the chip's GPIO registers to the serial monitor. In Phase 2, wheel encoders and a PID loop per motor hold the robot's speed, so it drives straight for 6 seconds. More phases are still to come.

    Problem

    The two motors are not equal. At the same 65% PWM with the wheels off the ground, the left wheel counted 339 and 340 pulses in 2 seconds and the right counted 398 both times. On the floor, a run without any correction curved sharply left, with the right wheel 210 pulses ahead (714 against 924).

    Approach

    Each wheel has a quadrature encoder whose A channel fires a rising-edge interrupt that adds to a counter, so pulses are counted while the main program checks the switch and adjusts PWM. Every 100 ms each motor runs its own PID loop against a target of 12 pulses per 100 ms, with its PWM kept between 40% and 80%. On top of that, a sync check every 5 ms cuts the right motor's forward drive for a moment whenever it is more than 2 pulses ahead.

    Phases

    1. Phase 1Done

      Switch-driven circles

      Each flip of an SPDT switch, debounced, makes the robot drive one smooth circle to that side, one wheel at 70% duty and the other at 40%, then stop and wait. The firmware prints the SIO GPIO_IN, GPIO_OUT and GPIO_OE registers before and during each move.

    2. Phase 2In progress

      Holding speed with encoders and PID

      Wheel encoders counted in interrupts, a PID loop per motor and a 5 ms sync check. The floor run drives straight with the wheels 2 pulses apart, and both encoder signals were checked on an oscilloscope. Still to do: the same run on a second surface, with the two travel distances within 5% of each other, and a video of both.

    3. Phase 3Upcoming

      Phase 3 title

      What the next phase adds to the robot

    How the robot drives

    One run

    Everything a run does, from the switch to the stop. Counting happens in interrupts, so it never waits on the rest.

    • check
    • tool
    • person
    1. 01 / 05person

      Switch on

      Flipping the switch, debounced at 300 ms, starts the run. The robot waits 2 seconds, then drives.

    2. 02 / 05tool

      Count pulses

      A rising edge on each encoder's A channel fires an interrupt that adds to a volatile counter for that wheel.

    3. 03 / 05tool

      PID every 100 ms

      Each motor compares its pulses in the last 100 ms with the target of 12 and sets its own PWM from the error, its sum and its change, kept between 40% and 80%.

    4. 04 / 05check

      Sync every 5 ms

      If the right wheel's total is more than 2 pulses ahead of the left, its forward drive is cut for a moment so the left can catch up.

    5. 05 / 05tool

      Stop after 6 s

      Both motors stop, and the serial monitor shows the counts and PWM for the run.

    Finding why it curved

    The robot kept curving left. Each test ruled something out before the fix.

    • check
    • tool
    1. 01 / 04check

      One motor at a time

      Running the motors one at a time confirmed the encoder assignments were right.

    2. 02 / 04check

      Same PWM, wheels up

      At 65% PWM with the wheels off the ground, the left wheel counted 339 and 340 pulses in 2 seconds, the right 398 both times: the motors themselves differ.

    3. 03 / 04tool

      Tuning alone

      Changing the target speed, the PID gains and the starting PWM, and lowering the right motor's PWM, still left a large curve to the left.

    4. 04 / 04check

      Add the sync check

      A second check on the encoder totals, alongside the PID loops, brought the gap from 210 pulses to 2, and the robot drove straight.

    Architecture

    • Switch to Main loop
    • Encoders to Interrupts, edges
    • Interrupts to PID loops, counts
    • Main loop to PID loops
    • Interrupts to Sync check
    • PID loops to PWM, duty
    • Sync check to Direction pins
    • PWM to H-bridge
    • Direction pins to H-bridge
    • H-bridge to Motors
    The switch starts a run and the encoders feed counts through interrupts. Two PID loops set each motor's PWM, the sync check trims the right motor, and the H-bridge drives the wheels.

    Gallery

    A GW Instek GDS-1152A-U oscilloscope showing two square waves, yellow for the left encoder and blue for the right, with a measurement panel listing 176.1 Hz and 172.4 Hz.
    Both encoders on the scope with the wheels raised: left (yellow, GP18) at 176.1 Hz and 52.81% duty, right (blue, GP20) at 172.4 Hz and 54.15%. 1 V and 5 ms per division.
    A close view of the oscilloscope screen with the two encoder square waves, one above the other.
    A closer look at the two channels. Each rising edge is one interrupt and one count.

    Challenges and solutions

    Challenge: The right motor was always faster.
    Solution: Lowering its PWM alone did not fix the curve. The final version pairs a PID loop per motor with a second check on the encoder totals that briefly holds back the right wheel when it gets more than 2 pulses ahead.
    Challenge: Counting every pulse while the program does other work.
    Solution: Encoder pulses are counted in rising-edge interrupts into volatile counters, so counting carries on while the main program checks the switch and adjusts PWM.
    Challenge: Seeing what the hardware is actually doing.
    Solution: Phase 1 prints the SIO GPIO_IN, GPIO_OUT and GPIO_OE registers before and during each move. Phase 2 prints counts and PWM over USB serial, which is how the late dip in both motors' pulses showed up, and an oscilloscope on both encoder channels confirmed clean square waves.

    Outcomes

    • Final run: 635 pulses left and 637 right, a gap of 2 (0.31%), down from 210 without the sync check, and the robot drove straight for the full 6 seconds.
    • Encoder signals checked on an oscilloscope: square waves at 176.1 Hz left and 172.4 Hz right with the wheels raised.
    • Switch-driven circles in Phase 1: one wheel at 70% duty and the other at 40% for 5 seconds.