RP2040-based controller for a megasonic/ultrasonic wafer cleaner. A NEMA17 arm sweeps the ultrasonic transducer across a wafer while a TMC2130 driver, an SPI LCD, and a rotary encoder provide a complete on-device UI. Motion is described in arm angle (degrees), and the sweep width is derived automatically from the selected wafer diameter and the arm length.
- RP2040 microcontroller – dual-core ARM, 133 MHz, 264 KB RAM
- Dual-core architecture – Core 0 runs the state machine and I/O; Core 1 renders the LCD at ~20 fps
- TMC2130 stepper driver – SPI-controlled, selectable StealthChop/SpreadCycle chopper, current limiting, adjustable run/park hold-current %, live fault detection (overtemperature, short-to-ground, charge-pump UV) and StallGuard collision detection (blocked arm → park + red COLLISION)
- Hardware SPI LCD – GMT147SPI 1.47" 172×320 ST7789 at 20 MHz on its own SPI bus
- Rotary encoder UI – KY-040 quadrature + push-button; full menu, in-place value editing, and a basic/advanced menu unlock
- Angle-based motion – park and centre angles in degrees; sweep angle computed from wafer Ø and arm length
- Configurable sweep – sweep time, wafer diameter, path (back-centre / back-front), and velocity profile (linear / harmonic / inverse-distance)
- Persistent settings – all parameters stored in flash EEPROM emulation and reloaded at boot; loading is forward/backward compatible, so a firmware update keeps your existing settings and only defaults newly-added ones
- Two operating modes – sensor-driven (spray valve + flow sensor) or a menu-driven Debug bypass mode
- Safety – re-home before every run, home-search timeout, ultrasonic energised only while the arm is over the wafer, park-then-disable stop, StallGuard collision park, and a recoverable ERROR state
See HARDWARE.md for:
- Pin assignments (TMC2130 on SPI1, LCD on SPI0, encoder, sensors)
- Wiring diagrams and power requirements
- TMC2130 driver configuration and
R_SENSE
pio run # Build
pio run --target upload # Flash to Pico (BOOTSEL/bootloader mode)pio device monitor --baud 115200Expected startup:
=== Stepper Controller Initializing ===
Settings: loaded from flash
Hardware initialized
SPI0 LCD initialized: SCK=18 MOSI=19
SPI1 TMC initialized: SCK=10 MOSI=11 MISO=12
TMC2130 configured: 600 mA, run hold 25%, park hold 10%, 256x microsteps, interpolation, StealthChop
Encoder initialized: CLK=26 DT=27 SW=22 (polled rotation, interrupt button)
Display initialized (GMT147SPI 1.47" 172x320)
Initialization complete!
State/position changes are echoed to serial by Core 1 (only on change):
State:IDLE | Pos:0 steps (0.0 deg) | Spray:OFF | Flow:NO
The encoder drives the whole interface: rotate to move the selection or change a value, click to select / enter-edit / confirm, and long-press to go back to the menu from the Sweep Settings / Setup screens (there is no "< Back" row). Item lists (main menu, Sweep Settings, Setup) wrap around — rotating past the last row selects the first, and vice versa.
- Basic menu (default):
START/STOPandSettings. An arm-position animation under the rows shows the wafer (circle), the park tick, the live arm position (red arrow), and a blinking lightning sign while the ultrasonic generator is energised. - Advanced menu: a short-click immediately followed by a long-press toggles the
advanced items (
SetupandAbout) on/off. - Sweep Settings: sweep time, wafer diameter, sweep type, and speed profile —
each row shows
label:valuein a large font with the value highlighted, and the arm animation (about a third of the screen height) sits underneath. The calculated sweep angle is shown in the side status bar. - Setup (hardware): a 20-row list that scrolls vertically — a 6-row window tracks
the selection, with a
row/totalcounter and ▲/▼ markers showing more above/below. Rows: park angle, centre angle (live jog while editing), Parking speed (deg/s — arm speed for homing/park/staging moves), arm length, gear ratio (Gear in/Gear outteeth, default 15:108), cycles, Accel (deg/s²), Jerk (deg/s³), Backlash take-up (µsteps injected on reversal), driver current, RunHold / PrkHold current (% of run), Chop (StealthChop ↔ SpreadCycle), microsteps (Mstep), Interp (microstep interpolation on/off), direction invert, the Debug toggle (ON= spray/flow ignored,OFF= spray/flow safety inputs active), and Stall / StallSG (StallGuard collision detection on/off + sensitivity). - About: firmware version and live TMC2130 driver status.
- Status bar (right side of every screen): live state, arm angle, the sweep summary (sweep angle, time, wafer, type, profile), and spray/flow.
Settings are stored in RP2040 flash EEPROM emulation and reloaded at boot. Empty or corrupt flash is initialized with defaults; compatible older records are accepted and rewritten with the current version when saved.
The controller is a non-blocking state machine with 7 states:
| State | Behavior |
|---|---|
IDLE |
Motor disabled; waiting for START (menu) or, in sensor mode, the spray valve |
HOMING |
Driving toward the limit switch to re-establish the zero reference |
PARKED |
Moving to / holding the park angle, then deciding the next step |
WAITING_SPRAY |
Spray on but no flow yet (sensor mode only); fan at 50 % |
SPRAY_ACTIVE |
Fan 100 %, generator on over the wafer; moving to the sweep start |
OSCILLATING |
Sweeping the arm across the wafer for the configured cycles |
ERROR |
Fault latched; motor disabled, yellow LED blinks. Recover with START |
See STATE_MACHINE.md for the full flow diagram and transition table.
The TMC2130 and the LCD are on separate SPI buses (no shared bus).
| Pin | GPIO | Function |
|---|---|---|
| CS | 13 | SPI chip select |
| SCK | 10 | SPI1 clock |
| MOSI (SDI) | 11 | Data out (controller → driver) |
| MISO (SDO) | 12 | Data in (driver → controller) |
| STEP | 14 | Step pulse |
| DIR | 15 | Direction |
| EN | 1 | Enable (LOW = active) |
| Board Label | GPIO | Function |
|---|---|---|
| CS | 9 | Chip select |
| DC | 5 | Data/Command |
| RES | 6 | Reset |
| SCL | 18 | SPI0 clock |
| SDA | 19 | SPI0 data (MOSI) |
| BL | 20 | Backlight (HIGH = on) |
| Board Label | GPIO | Function |
|---|---|---|
| CLK | 26 | Quadrature A |
| DT | 27 | Quadrature B |
| SW | 22 | Push-button (LOW when pressed) |
| Signal | GPIO | Function |
|---|---|---|
| Limit | 28 | Home limit switch (INPUT_PULLUP, LOW when pressed) |
| Spray | 2 | Spray-valve status (INPUT_PULLDOWN, HIGH = active) |
| Flow | 3 | Flow sensor (INPUT_PULLDOWN, HIGH = flowing) |
| LED_G | 8 | Green status LED |
| LED_Y | 7 | Yellow status LED |
| Fan | 21 | Fan PWM (0–255) |
| Sonic | 4 | Ultrasonic generator relay (active-low, LOW = ON) |
See HARDWARE.md for complete wiring details.
Spray/flow sensors are read when Debug is OFF (SENSOR_INPUTS_ENABLED = true).
In the default Debug ON mode the spray/flow inputs are ignored and the cycle is driven
entirely from the menu.
- Purpose: home/reference position detection
- Activation: LOW (to GND when pressed); 20 ms debounce
- Used in:
HOMINGto establish the zero position; always re-homed before a run
- Purpose: detect cleaning-fluid spray activation
- Activation: HIGH (3.3 V when the valve opens)
- Used in:
IDLE → HOMINGtrigger (sensor mode)
- Purpose: detect liquid flow through the system
- Activation: HIGH (3.3 V when flowing)
- Used in:
PARKED/WAITING_SPRAY → SPRAY_ACTIVE(sensor mode)
Motion is expressed as arm angle rather than linear travel. Steps are derived from the angle, the motor's full-steps-per-revolution, the current microstep setting, and the gear reduction between the motor pinion and the arm output gear:
steps = degrees × FULL_STEPS_PER_REV × microsteps × (gearOutTeeth / gearInTeeth) / 360The gear ratio is configurable in Setup (Gear in / Gear out teeth). The default is
15:108 (= 7.2:1), so the motor turns 7.2 steps per arm degree. All motion quantities
are expressed in the arm frame — position (angles), velocity (sweep cycle time and the
Parking speed for homing/park/staging), acceleration and jerk — so they stay physically
meaningful when the gearing or microstepping changes. The constant-speed positioning moves
derive their motor step rate from Parking speed (deg/s), clamped to the motor's fastest
safe rate, and the homing timeout scales automatically with the resulting step rate.
Default parameters (all editable on-device and persisted to flash):
const int FULL_STEPS_PER_REV = 200; // 1.8° NEMA17
int gearTeethMotor = 15; // motor pinion teeth
int gearTeethOutput = 108; // arm output gear teeth (15:108 = 7.2:1)
int PARK_DEG_X10 = 50; // 5.0° — park angle near the limit
int CENTER_DEG_X10 = 700; // 70.0° — sweep centre (over wafer)
int ARM_LENGTH_MM = 250; // arm length (transducer radius)
int backlashMicrosteps = 0; // extra µsteps injected on each direction reversal
unsigned long SWEEP_TIME_MS = 4000; // time for one full back-forward-back cycle
unsigned long OSCILLATION_CYCLES = 4; // full cycles to run (0 = run forever)The sweep angle is computed from the selected wafer diameter and the arm length so the sweep extremes land on the wafer edges:
sweep = 2 · asin( (wafer_diameter / 2) / arm_length )
- Sweep type
Edge↔(•): arm travels edge → centre (half the sweep). - Sweep type
Edge↔Edge: arm travels edge → edge (full sweep). - Speed profile:
Sawtooth,Sine, orReciprocal— velocity-shaping across each sweep.Sawtoothis constant speed throughout.SineandReciprocalare both fastest at the wafer centre and slow toward the edges (independent of sweep type) —Sinefollows a cosine taper,Reciprocalan exponential decay — keeping dwell-time/area roughly constant as the wafer spins beneath the arm. All profiles ease in/out at each direction reversal, with a ramp width derived from the configured endpoint acceleration (so faster profiles automatically get a wider deceleration zone) and a shape blended by the configured endpoint jerk (smoother/quintic at low jerk, sharper/cubic at high jerk). Both are adjustable in the Setup menu (Accel/Jerk) or over serial (a/A,j/J), and persist to flash.
Backlash compensation: if the gear train has slack, set Backlash (in Setup) to the
number of extra microsteps to inject on each direction reversal. These steps move the
motor but not the load, taking up the slack so the arm reaches the commanded angle.
Collision / stall detection. With Stall enabled, the TMC2130's StallGuard2 watches
the motor load while sweeping. If the arm is blocked or stalls (e.g. you stop it by hand),
the firmware moves to park, disables the motor, and latches ERROR with a red
COLLISION message on screen. StallSG sets the sensitivity (SGT, −64 = most sensitive
… +63 = least); tune it on the bench. StallGuard is only reliable in SpreadCycle, so set
Chop = Spread for collision detection to work (it is ignored in StealthChop). Press START
to recover (re-homes first).
Every move is acceleration-limited. The sweep uses the jerk-limited 7-phase S-curve;
all the other moves — homing, parking, pre-sweep staging and the live centre jog — use a
trapezoidal profile (profiledMove()): velocity ramps up at Accel, cruises at
Parking speed, then ramps down to stop exactly on target. Nothing starts or stops at a
hard step, so the arm no longer jumps to a new value and loses steps (notably when setting
the wafer centre). All limits are derived from a single stepsPerArmDeg() factor that folds
in steps-per-rev, the microstep setting and the gear ratio, so speed, acceleration and
position all stay consistent in the arm frame.
The ultrasonic generator is energised only while the arm tip is over the wafer disk.
- earlephilhower/arduino-pico – RP2040 Arduino core: dual-core
setup1()/loop1(), hardware SPI, accurateanalogWrite - TMCStepper (teemuatlut) – TMC2130 SPI interface
- Adafruit GFX Library – graphics primitives
- Adafruit ST7735/ST7789 – LCD driver (
Adafruit_ST7789)
These libraries are declared in platformio.ini so PlatformIO can
resolve them automatically during pio pkg install / pio run.
- Non-blocking dual-core state machine
- Angle-based motor control (step/dir) with microstepping
- TMC2130 driver configuration over SPI1, with run/park hold-current modes
- Driver fault detection (OT / short-to-ground / charge-pump UV) with park-then-disable
- Home-search safety timeout and re-home before every run
- Sensor reading (limit, spray, flow) with debounce + sensor-bypass DEBUG mode
- LED indicators, fan PWM, ultrasonic relay (energised only over the wafer)
- LCD UI — hardware SPI 20 MHz, partial redraw, ~20 fps on Core 1, arm-position animation
- Rotary encoder — polled quadrature (RC-filtered inputs) with interrupt button, acceleration, cyclic menu navigation, basic/advanced menu unlock
- On-device Settings/Setup editors
- Persistent settings in flash (versioned, checksummed, compatible record loading)
- Recoverable ERROR state (START re-homes and clears the fault latch)
- StallGuard-based stall/load detection
- Per-profile sweep tuning UI
| Issue | Cause | Fix |
|---|---|---|
| No serial output | Baud mismatch | Use 115200 baud |
| Motor won't move | EN pin HIGH | Check GPIO 1 is LOW when enabled |
| Won't start in DEBUG mode | Not at a known position | Use START — it re-homes first |
| Stuck in IDLE (sensor mode) | Spray sensor LOW | Check GPIO 2 reads HIGH, or set Debug = ON |
| ERROR right after start | Driver fault or home not found | Check TMC2130 (About screen) and limit switch wiring; press START to retry |
| Stuck in HOMING | Limit never pressed | Check GPIO 28; home search times out after 70° → ERROR |
| LCD not showing | Wrong pins or lib | See HARDWARE.md (LCD is on SPI0: SCK 18, SDA 19) |
MIT — see LICENSE.