Software component of a capstone design project. Streams GDS2 lithography patterns to an FPGA over UART for motor-driven stage control.
Team: Jack Parrack, Laurel Leuwerke, Matt Bracker, Chance Besancenez
RAPID reads a GDS2 input file, converts the XY coordinates to polar form, and transmits each point as a framed binary packet over a serial COM port to a Zynq FPGA. The FPGA drives a stepper motor (radial axis) and laser to write the pattern on a spinning disc. The PC waits for each ACK before sending the next point (stop-and-wait flow control). A Python GUI provides real-time visualisation of the acknowledged points and full manual hardware control.
input.gds ──► inputParser ──► RAPID.exe ──► UART ──► systemControl
(XY -> polar) (frame + send) (recv + motor control)
▲ │
gui.py ◄──── ACK log ───────────────┘
(live XY scatter)
systemControl ──► AXI GPIO
──► stepperDriver.vhd
──► spindle.vhd
──► LaserEn (pin P18)
RAPID/
├── src/
│ ├── main.c # PC-side serial manager (compiled -> build/RAPID.exe)
│ ├── inputParser.c/h # GDS2 parser: XY coordinates -> polar points
│ ├── serial.c/h # Shared Win32 serial utilities
│ ├── packets.c/h # PC-side packet constructors (send_polar_point, send_ctrl_packet, etc.)
│ ├── protocol.h # Shared wire protocol constants
│ └── gui.py # PySide6 real-time visualisation + manual control GUI
├── vitis_workspace/ # Xilinx Vitis workspace (FPGA software)
│ ├── platform/ # BSP platform project (generated from .xsa)
│ └── systemControl/ # Active FPGA app — UART receiver + motor/laser control
├── hardware/ # Xilinx Vivado project files (FPGA hardware)
│ ├── RAPID.srcs/sources_1/new/
│ │ ├── stepperDriver.vhd # Stepper motor FSM (DRV8834)
│ │ ├── spindle.vhd # BLDC 6-step commutation (DRV8323)
│ │ └── VoiceCoil.vhd # Dual voice-coil PWM driver (~32 kHz)
│ └── RAPID.srcs/constrs_1/new/RAPID.xdc # Pin constraints (Arty Z7-20)
├── PCB/ # PCB schematic/layout designed with Altium Designer
│ ├── Spindle # BLDC spindle motor custom PCB files
│ └── Stepper # Stepper motor custom PCB files
├── tests/ # Untested drivers / integration code
│ ├── mlx90393.c # Bare-metal XIicPs driver for MLX90393 magnetometer (untested on hardware)
│ └── mlx90393.h # Header: pin wiring, register definitions, calibration constants, API
├── old/ # Archived / prototype code (not active)
│ ├── angleMeasure.ino # Arduino sketch — MLX90393 magnetometer angle tracking (prototype, untested in system)
│ ├── BLDC.vhd # Early abstract BLDC design
│ ├── OLD_pcCommunication.c # Superseded PC communication code
│ └── OLD_systemControl.c # Superseded FPGA application
├── docs/
│ ├── README.md # This file
│ └── CLAUDE.md # Full technical context for AI-assisted development
├── build/ # Compiled output — generated by make, not committed
├── input.gds # Default GDS2 input file
├── Makefile # PC-side build
└── requirements.txt # Python dependencies
| Tool | Purpose |
|---|---|
| MinGW-w64 / MSYS2 UCRT64 | C compiler (gcc) and make |
| Python 3.11+ | GUI |
| PySide6 6.x | Qt GUI framework |
| pyqtgraph | Real-time plotting |
Install Python dependencies (preferably inside a venv):
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
gcc and make are provided by MSYS2. If make is not on your PATH, add C:\msys64\usr\bin to your user environment variables.
- Xilinx Vivado 2025.1 (or compatible)
- Xilinx Vitis 2025.1 (or compatible)
- Target device: Arty Z7-20 (xc7z020clg400-1)
vitis_workspace/systemControl/main.cmust be built inside a Vitis bare-metal project — it is not part of the PC Makefile.
- Open the Vivado project: File → Open Project →
hardware/RAPID.xpr - In the Sources panel, expand Design Sources and locate the block design
top.bd - Generate the HDL wrapper:
- Right-click
top(the.bdfile) → Create HDL Wrapper… - Select "Let Vivado manage wrapper and auto-update" → OK
- Right-click
- Set the wrapper as the top module:
- Right-click the newly created
top_wrapper→ Set as Top
- Right-click the newly created
- If IP are out of date: click Reports → Report IP Status → select all → Upgrade Selected
- Run Synthesis → Implementation → Generate Bitstream (Flow Navigator)
- Export the hardware definition:
- File → Export → Export Hardware…
- Check Include bitstream → save as
top_wrapper.xsa
The Vitis workspace is set up at vitis_workspace/ with two components:
- platform — BSP platform project (built from the exported
.xsa) - systemControl — active application: receives packets and drives motors/laser
- Launch Vitis and open the workspace: File → Open Workspace →
vitis_workspace/ - Rebuild the platform if the
.xsachanged: right-click platform → Build - Build the application:
- Right-click systemControl → Build Project
- Program the FPGA & run:
- Connect the Arty Z7 via USB
- Right-click systemControl → Run As → Launch on Hardware
Note: The XDC constraints file at
hardware/RAPID.srcs/constrs_1/new/RAPID.xdccontains all pin and I/O standard assignments. If you change block design port names, update the XDC to match.
make
Output: build/RAPID.exe
make gui
make gui automatically uses the venv Python at venv/Scripts/python if the venv exists, and falls back to the system Python otherwise. To launch directly:
python src/gui.py
The GUI lets you:
- Connect to the serial port and manage
RAPID.exeas a subprocess - Select a GDS input file, preview its XY coordinates without hardware, and stream a pattern with live ACK plotting — the sled is automatically zeroed before each stream
- Toggle spindle and laser, jog the stepper, send a zero request — all from the Manual Control tab
- Set voice coil 1 duty cycle (0–100%) from the Manual Control tab
- Stream a pattern multiple times (repeat count spinbox) without stopping the spindle between repetitions
- Monitor spindle RPM on a live time-series plot in the RPM Monitor tab
- Monitor ACK / CRC error counters and a scrolling output log
make clean
Both main.c (PC) and systemControl/main.c (FPGA) use the same framing:
[ 0xAA | 0x55 | TYPE | LEN | PAYLOAD (LEN bytes) | CRC8 ]
| Direction | TYPE | LEN | Payload | Purpose |
|---|---|---|---|---|
| PC → FPGA | 0x01 |
0 | (none) | End of sequence — disables hardware, firmware stays running |
| PC → FPGA | 0x10 |
8 | r_um (int32 LE, µm) + theta_deg (float32 LE, degrees) |
Polar point |
| PC → FPGA | 0x21 |
1 | 0x00/0x01 |
Spindle off/on |
| PC → FPGA | 0x22 |
1 | 0x00/0x01 |
Stepper off/on |
| PC → FPGA | 0x23 |
1 | 0x00/0x01 |
Laser off/on |
| PC → FPGA | 0x24 |
1 | 0x00=inward, 0x01=outward |
Stepper direction |
| PC → FPGA | 0x25 |
0 | (none) | Zero request |
| PC → FPGA | 0x26 |
4 | int32 LE step count | Jog — move N steps in current direction |
| PC → FPGA | 0x27 |
0 | (none) | RPM request — FPGA reads hall sensor period and responds |
| PC → FPGA | 0x28 |
1 | uint8 (0–100) | Voice coil 1 duty cycle % |
| FPGA → PC | 0x81 |
8 or 0 | Echo of received payload | ACK for all packet types |
| FPGA → PC | 0x82 |
2 | uint16 LE | Computed spindle RPM (10000 / hall_period_ticks) |
| FPGA → PC | 0xF0 |
N | ASCII string | Debug / status message |
CRC8 is computed as XOR over [TYPE, LEN, PAYLOAD...].
Flow control: Stop-and-wait. Each packet waits up to 5 s for ACK.
TYPE_END: Disables laser, spindle, and stepper. The firmware ACKs and then continues the receive loop — it does not halt. Multiple pattern runs and manual commands work within one FPGA session.
Theta tracking:
systemControl/main.cuses time-integration (theta_init()/wait_for_theta()) to stall each point until the disc reaches its target angle.theta_init()snapshots the hall-sensor RPM period and a Zynq global-timer timestamp; subsequent calls compute disc angle as(elapsed_us % rev_us) / rev_us × 360°. The laser is gated off between points when the remaining arc exceeds 50 ms to avoid unintended exposure during long cross-revolution waits.
systemControl/main.c drives the motor hardware via a packed 27-bit value written to AXI GPIO Channel 1:
| Bits | Field | Description |
|---|---|---|
| [0] | spindle_en |
Spindle enable: 0=off, 1=on |
| [1] | stepper_dir |
Stepper direction: 0=inward, 1=outward |
| [2] | stepper_en |
Stepper enable: 0=off, 1=on |
| [3] | zero_req |
Zero/home request: momentary high pulse |
| [24:4] | num_step |
Number of steps to move (0–2,097,151) |
| [25] | step_go |
Step go: momentary high pulse triggers move |
| [26] | laser_en |
Laser enable: 0=off, 1=on |
The firmware enters a packet receive loop immediately — no startup zeroing delay (the VHDL ZEROING state homes the sled at power-on). When streaming, RAPID.exe always zeros the sled before the first point is sent.
| Packet | Action |
|---|---|
TYPE_POINT |
Compute target step = round(r_um / 30000 × 250), move stepper (dir set automatically), turn laser on after first move, ACK |
TYPE_END |
Disable laser/spindle/stepper, reset state, ACK, continue loop |
TYPE_SPINDLE/STEPPER/LASER/DIR |
Update corresponding GPIO bit, ACK |
TYPE_ZERO |
Pulse zero_req 100 ms, reset current_step=0, ACK — ignored if stepper is disabled |
TYPE_JOG |
Move N steps (int32 LE payload) in current direction, ACK — ignored if stepper is disabled |
TYPE_RPM_REQ |
Read hall sensor period from axi_gpio_1, compute RPM = 10000 / ticks (6 pulses/rev, 1 kHz tick clock), respond with TYPE_RPM |
TYPE_VC1_DC |
Write 0–100 duty cycle value to axi_gpio_1 ch2 → VoiceCoil.vhd VC1_DC port, ACK |
250 steps = 30 mm (full disc range, inner to outer edge). The stepper step rate is 500 Hz (2 ms/step).
Spindle windup: At FPGA boot the firmware enables the spindle and waits 2 seconds for rotor alignment and speed. When streaming,
RAPID.exeadditionally waits 1 second after re-enabling the spindle before sending the first point. The laser turns on after the first point's stepper move completes.
RPM readout:
spindle.vhdmeasures the period between consecutive hall sensor pulses using a 1 kHz tick counter and exposes the result viaaxi_gpio_1ch1. The firmware computesRPM = 10000 / ticks(6 hall pulses per revolution) and logs it after every point during a stream. The PC can also request an on-demand reading by sendingTYPE_RPM_REQ (0x27).
Voice coil:
VoiceCoil.vhdgenerates a ~32 kHz PWM signal for VC1 (vertical / Z-axis). Duty cycle is software-controlled viaaxi_gpio_1ch2 and theTYPE_VC1_DCpacket. Firmware boots at 60% duty cycle. VC2 (horizontal) is present in hardware but not yet driven.
The current theta tracking uses pure time-integration of RPM (accuracy ~1°/rev at 1% RPM error). A complete bare-metal Xilinx PS I2C driver for the MLX90393 3-axis magnetometer exists in tests/mlx90393.c / tests/mlx90393.h — untested on hardware. The algorithm reference is the Arduino sketch in old/angleMeasure.ino.
The driver communicates over PS I2C0, wired to the Arty Z7-20 Arduino-compatible header (SCL → A5 / P15, SDA → A4 / P16). It uses the same empirically derived calibration constants as the Arduino sketch and tracks accumulated angle via cross/dot-product of consecutive XY vectors (numerically stable through wrap-around).
To integrate into systemControl/main.c:
- Add
tests/mlx90393.candtests/mlx90393.hto the Vitis project. - Call
mlx_init(&mlx, XPAR_XIICPS_0_BASEADDR)at startup after the spindle windup. - Replace or supplement
wait_for_theta()with periodicmlx_read_angle()calls to get drift-free absolute angle.
The calibration constants are hardcoded and were derived empirically — a re-calibration step will be needed if the magnet is repositioned.
VoiceCoil.vhd already contains a fully wired PWM generator for VC2 (DC_cnt_2, clk_div_cnt2, PWM_2_sig) and the XDC pin assignments are defined (PWM2 → V6, PWM2r → U7). The channel is hardcoded to 0% duty cycle pending the following steps:
- Add
VC2_DC : in STD_LOGIC_VECTOR(6 downto 0)to the VoiceCoil entity port list, removeDC_cnt_2 <= 0, and addDC_cnt_2 <= to_integer(unsigned(VC2_DC)) * 39. - Connect
VC2_DCto a new AXI GPIO output channel in the Vivado block design and re-export the hardware definition. - Add
TYPE_VC2_DC 0x29tosrc/protocol.handvitis_workspace/systemControl/protocol.h. - Add a handler in
vitis_workspace/systemControl/main.c(mirror theTYPE_VC1_DChandler). - Add a
VC2 <0-100>command insrc/main.cand a matching GUI control insrc/gui.py.
The spindle already generates 6 hall pulses per revolution (known 60° sector boundaries). Rather than relying solely on time-integration, wait_for_theta() in systemControl/main.c could latch each hall rising edge (via a new interrupt or polled GPIO) and snap the theta reference to the nearest known sector angle, eliminating long-run drift without any additional sensors.
Commutation is currently open-loop (fixed 10 Hz clock in spindle.vhd). The hall-sensor RPM measurement already available in the firmware could drive a simple proportional controller that adjusts count_max in the VHDL to maintain a target RPM — making theta tracking more accurate and the pattern write speed more consistent.
stepperDriver.vhd exports step_total_out (21-bit absolute step counter from home), which is connected to axi_gpio_0 channel 2 (configured as input). The firmware currently does not read it. Reading step_total_out after each move and comparing it to current_step would detect missed steps and allow recovery homing.