A 3-degree-of-freedom robot arm, simulated entirely on an FPGA board and rendered live in an isometric 3D view over HDMI at 60 FPS — driven by a USB keyboard, solved with an from-scratch inverse-kinematics solver, and wrapped in a pick-and-place game with a live score.
Technical Highlights
The system splits cleanly into two halves that talk over AXI: a MicroBlaze soft-core processor handling USB input, target tracking, inverse kinematics, and the pick-and-place game logic in C, and a SystemVerilog hardware pipeline handling the register file, CORDIC math, isometric projection, and the HDMI signal itself. Every piece of live state — joint angles, target and object positions, score, and game state — is written one-directionally from the MicroBlaze into an AXI4-Lite register file, which the hardware side reads and never writes back to.
Fig. 1 — full datapath: USB input → MicroBlaze software pipeline → AXI register file → SystemVerilog hardware pipeline → HDMI output.
Offset Register Description 0x00 Theta1 Shoulder Yaw Angle 0x04 Theta2 Shoulder Pitch Angle 0x08 Theta3 Elbow Pitch Angle 0x0C Target_X End Effector Target X 0x10 Target_Y End Effector Target Y 0x14 Target_Z End Effector Target Z 0x18 Obj_X Object X Position 0x1C Obj_Y Object Y Position 0x20 Obj_Z Object Z Position 0x24 Score Score Value 0x28 State Pick and Place State
The AXI4-Lite register file — the entire hardware/software interface, 11 registers wide.
The Vivado block design underneath ties the MicroBlaze, its AXI
interconnect, the UART and USB peripherals, and the custom HDMI
controller IP together — the actual hardware described above lives
inside that hdmi_text_controller_0 block.
Fig. 3 — the full Vivado block design: MicroBlaze, AXI interconnect, peripheral IP, and the custom HDMI controller.
The hardware renders a true 3D world in a 2D isometric view using a fixed 30° camera transform. Given a 3D point (x, y, z), the transform to 2D screen coordinates is:
xdisplay = (x − z) · cos(30°)
ydisplay = −y + (x + z) · sin(30°)
Fig. 2 — the isometric coordinate transform this rendering pipeline is built on.
Those screen coordinates are computed downstream of the CORDIC module, which supplies the sine and cosine of each joint angle needed to turn a joint angle into a 3D vector in the first place. Four pairs of screen coordinates come out the other end of the pipeline — base, shoulder, elbow, and end effector — which the renderer connects with three 2-pixel-wide line segments (base→shoulder, shoulder→elbow, elbow→end effector), extending the line-drawing logic built for pixel drawing in an earlier lab. A pixel is colored in if it falls within a 2-pixel Manhattan distance of any active segment, which is what keeps the arm links solid and gap-free. The actual rendered output is shown at the top of this page.
SystemVerilog has no native trigonometric functions, so sine, cosine, and atan2 — all required for the isometric transform and the inverse kinematics — are computed by a custom CORDIC (COordinate Rotation DIgital Computer) module: a shift-and-add algorithm that approximates a rotation through a sequence of small, predefined angle steps using only a precomputed atan lookup table, with no floating-point hardware anywhere in the design. The module runs in two modes — rotation (given a vector and an angle, rotate it) and vectoring (given a vector, find its angle) — each converging over 16 add-shift iterations.
Fig. 7 — simulation waveform covering all 8 CORDIC test cases, rotation and vectoring mode.
Test Mode Input Output 1 Rotation (1,0) by π/4 cos/sin = 0.707 / 0.707 2 Rotation (1,0) by π/6 cos/sin = 0.866 / 0.500 3 Rotation (1,0) by 0 1.0 / 0.0 (unchanged) 4 Vectoring atan2(1, 1) π/4 = 0.785 rad 5 Vectoring atan2(1, 0) π/2 = 1.571 rad 6 Vectoring atan2(0, 1) 0 rad 7 Vectoring atan2(0, -1) π = 3.142 rad 8 Vectoring atan2(-1, -1) -3π/4 = -2.356 rad
All 8 simulated test cases matched their expected outputs exactly.
Rather than solve the full 3-DOF problem at once, the IK solver decomposes it into a 1-DOF shoulder yaw rotation followed by a 2-DOF planar problem in the vertical plane containing the target: yaw comes directly from the ground-plane projection of the target, and the shoulder pitch and elbow pitch are solved from the law of cosines.
// shoulder yaw theta[0] = atan2f(target.y, target.x); float r = sqrtf(target.x * target.x + target.y * target.y); float h = target.z - shoulder_height; float D = (r * r + h * h - L1 * L1 - L2 * L2) / (2.0f * L1 * L2); D = clampf(D, -1.0f, 1.0f); // elbow pitch theta[2] = atan2f(-sqrtf(1.0f - D*D), D); // elbow-down theta[1] = atan2f(h, r) - atan2f(L2 * sinf(theta[2]), L1 + L2 * cosf(theta[2]));
The full inverse-kinematics solve, in C, running on the MicroBlaze.
A target position handler sits in front of the solver: it reads USB keycodes (w/a/s/d for the X-Y plane, e/q for Z) and applies a signed 0.05-unit delta to the current end-effector target along the corresponding axis, clamped to the arm's feasible range of motion. Rather than writing the IK solution straight to the register file — which would make the arm snap instantly to each new target — a joint angle interpolator moves each joint toward its solved target by a bounded step every frame, which is what makes the arm's motion read as continuous and smooth instead of jumpy.
On top of the arm and IK stack sits a 6-state finite state machine — IDLE, APPROACH PICK, PICK, APPROACH PLACE, PLACE, SCORE — that turns the simulator into an actual game. The first three states are gated on the distance from the end effector to the object; the last three are gated on the distance from the object to the target drop-off position. Crossing a pick-radius threshold advances the state, and completing a full pick-and-place cycle increments the score and loops back to start a new round.
Fig. 6 — the 6-state pick-and-place finite state machine.
In the final demo, the full loop worked end to end: the arm solved its own inverse kinematics correctly in 3D space, rendered that motion live over HDMI in isometric view, and the pick-and-place FSM correctly tracked an object from pickup through placement and scored it. Two things were genuinely harder than expected: Vivado repeatedly synthesized against a cached, stale version of the custom IP instead of the latest edits, so bug fixes silently failed to show up in hardware until the cache issue was tracked down — a persistent annoyance through most of development — and wiring the USB keyboard into the rest of the datapath took more effort than the earlier labs it was adapted from suggested it would.