A 2-axis pen plotter controlled by a BeagleBone Y-AI. Reads x y penStatus commands from a file and traces them with two stepper-driven axes and a servo-actuated pen lift. Multithreaded HAL/app split with a live progress display in terminal.
Self-directed Semester Project for ENSC 351 (Embedded & Real-Time Software)
| Part | Notes |
|---|---|
| BeagleBone Y-AI (BBY-AI) | ARM, drives everything over GPIO |
| 2 × NEMA 17 | X and Y axes |
| 2 × TMC2209 | Stepper drivers, step/dir from BBY-AI GPIO |
| SG90 servo | Pen lifter |
| 12 V 5 A brick | Powers both stepper drivers directly |
| 12 V → 5 V buck | Supplies the servo (onboard 5 V rail can't source enough current) |
Pen contact pressure is set by a rubber band on the lifter arm ... keeps the tip planted without binding the vertical motion.
Pin assignments and travel limits live in hal/include/hal/gpio_config.h.
Threads (5 total):
- main — init, drives the main loop, cleanup
- X motor — condvar-waits for a target, runs the move
- Y motor — condvar-waits for a target, runs the move
- servo — regenerates PWM pulses continuously to hold the SG90 against drift
- stats display — 500 ms poll of
plotter_stats, prints the status line
Per-axis threads let X and Y move in parallel — a target like (1000, 1000) traces a diagonal instead of going fully across in X and then in Y.
Each motor controller owns its worker via a mutex-protected has_target/is_moving state machine. Main blocks on both before changing pen state, so the pen never lifts mid-move.
Startup
- Install
SIGINThandler soCtrl-Ccan lift the pen and home cleanly instead of leaving the gantry mid-stroke. - Init GPIO via libgpiod, configure stepper step/dir pins, raise the servo to pen-up.
- Spawn the X, Y, servo, and stats threads.
Command loop — for each line of plot_commands.txt:
- Clamp target X/Y against the axis limits in
gpio_config.h. - If the pen state differs from current, command the servo and wait 200 ms for it to settle before any motion.
- Push targets to both motor controllers simultaneously.
- Block until both axes report
is_moving == false— guarantees the pen never changes state mid-move. - Bump the progress counter and publish a fresh snapshot to
plotter_stats.
Shutdown
- Lift the pen so it doesn't drag on the way home.
- Send both axes back to
(0, 0)and wait. - Signal all threads to stop and join them.
- Release GPIO lines, destroy mutexes/condvars, free everything.
.
├── CMakeLists.txt
├── app/
│ ├── include/ main.h, plotter.h, motor_controller.h, servo_controller.h, plotter_stats.h
│ └── src/ main.c, plotter.c, motor_controller.c, servo_controller.c, plotter_stats.c
├── hal/
│ ├── include/hal/ gpio.h, gpio_config.h, stepper.h, servo.h
│ └── src/ gpio.c, stepper.c, servo.c
└── plot_commands.txt
One move per line, whitespace-separated:
x y pen
0 0 0
1200 0 0
1200 -800 1
0 -800 1
0 0 0
x / y are signed step counts; pen is 1 (down) or 0 (up). # and blank lines are skipped. Out-of-range coordinates are clamped to the axis limits in gpio_config.h.
Plot files are generated from a separate Python tool that vectorises text using the Hershey fonts — single-stroke fonts originally designed for CNC plotters, so each glyph is one continuous pass rather than an outline that would need filling.
sudo ./PenPlotsudo is required for GPIO access unless the user is in the gpio group. Ctrl-C lifts the pen, returns to home, and exits cleanly.
Open-loop motion. Currently, there are no physical encoders; position is drelatively tracked from a home position set on initial startup. A missed step desyncs the drawing until the next home cycle.