Building Suniyo: An 8-DOF Quadruped Robot Dog with IK and Autonomous Navigation
Published on May 3, 2026 · Robotics, Embedded Systems, Kinematics, ESP32
A quadruped robot is one of those projects that sounds manageable until you actually start. Four legs, two joints each — eight servos total. But the moment you fire everything up, you realize the geometry is brutal. Servos are mechanically asymmetric: the right-side hip has to move in the opposite physical direction to produce the same semantic motion as the left side. Calibration drifts. IK blows up at the joint limits. And a gait that looks smooth on paper produces a robot that falls over.
This is the story of Suniyo — my 8-DOF quadruped robot dog. Two hip + knee joints per leg, a 2D inverse kinematics solver, three distinct gait patterns, dual ESP32 hardware, and a camera-based autonomous navigation system that decides whether to step over an obstacle or go around it.
The Hardware Architecture
The core issue with a quadruped is power and signal separation. I ran into this early: an ESP32-CAM cannot drive a PCA9685 I2C bus reliably while streaming video — the camera DMA and I2C transactions fight for bus time and you get stuttering servos or corrupted frames. The solution was to split the system across two boards:
The PCA9685 PWM driver sits on GPIO 21 (SDA) and GPIO 22 (SCL) of the servo ESP32. It runs at its default I2C address (0x40). Each of the 8 servo channels maps to a leg joint:
| Channel | Joint | Direction |
|---|---|---|
| 0 | Front Left Hip | +1 (normal) |
| 1 | Front Left Knee | +1 (normal) |
| 2 | Front Right Hip | -1 (mirrored) |
| 3 | Front Right Knee | +1 (normal) |
| 4 | Back Left Hip | +1 (normal) |
| 5 | Back Left Knee | +1 (normal) |
| 6 | Back Right Hip | -1 (mirrored) |
| 7 | Back Right Knee | +1 (normal) |
Channels 2 and 6 (both right-side hips) have direction -1. This matters: "forward" for the left hip means the servo rotates in one direction, but on the right side the servo is physically mounted as a mirror image, so the same semantic "forward" requires the servo to rotate the other way. The direction table handles this without touching the IK math.
The Inverse Kinematics Solver
Each leg is a 2-link planar arm — femur (130mm) and tibia (150mm). Given a target foot position (x, y) in the leg's local 2D plane, I need to find the hip and knee angles that place the foot there. This is the classic 2-DOF IK problem, solved with the law of cosines:
The clamping on d is critical. If the target foot position is outside the robot's reachable workspace — d > FEMUR + TIBIA or d < |FEMUR - TIBIA| — the acos argument goes outside [-1, 1] and the function raises a math domain error. The clamp silently snaps to the nearest reachable point instead of crashing.
Once IK gives angles in radians, they're converted to degrees and then to PWM offsets using a calibrated DEGREES_PER_PWM = 0.6 ratio (180° ÷ 300 PWM units for a standard servo). Per-servo calibration offsets are added before the PWM conversion to compensate for mechanical zero-point drift.
Three Gaits — Walk, Trot, Crawl
A gait is a pattern of phase offsets that determines when each leg lifts off the ground. Each leg cycles through a sine wave parameterised by the robot's time phase. The foot's x position during the stride follows cos(θ); when sin(θ) > 0 the foot lifts by STEP_HEIGHT (40mm).
The walk gait (phase offsets of π/2 between legs) keeps three feet on the ground at all times — statically stable, slow. The trot gait (diagonal pairs in sync: FL+BR, FR+BL) only has two feet down at once — faster but requires dynamic balance. The crawl gait uses π/3 spacing — the most conservative, one leg at a time.
The stride length and height are tunable constants. During obstacle avoidance, the stride is asymmetrically scaled per leg: outer legs (relative to the turn direction) get 1.5× stride, inner legs get 0.5×, producing a smooth arc rather than a pivot.
Camera-Based Obstacle Avoidance
The ESP32-CAM streams MJPEG over HTTP to the Python controller. The stream is parsed manually — scanning the raw byte buffer for JPEG SOI (0xFF 0xD8) and EOI (0xFF 0xD9) markers and decoding each frame with OpenCV. This avoids the reliability issues of OpenCV's built-in VideoCapture with HTTP streams. The system also has a Mac webcam fallback (cv2.VideoCapture(0)) for testing without the robot powered on.
Obstacle detection on each frame runs a pipeline: Gaussian blur → Canny edge detection (thresholds 50/120) → morphological close to fill edge gaps → contour extraction. Each contour is then filtered by three criteria before it counts as an obstacle:
- Area > 300 px² — ignores noise and small specks
- Aspect ratio between 0.2 and 5 — filters out thin lines and wires
- Solidity > 0.3 (contour area ÷ convex hull area) — rejects irregular, fragmented shapes
Surviving contours are scored by proximity: 70% weight on vertical position in the frame (lower = closer) and 30% on bounding box area. The top 5 closest obstacles by score are passed to the navigation system. A contour whose centre is above 25% from the top of the frame is discarded entirely — too far to matter. Position (left/center/right) is determined by dividing the frame width into thirds.
The navigation state machine has five cases, executed with a 3-second decision cooldown to prevent oscillation:
The step-over logic checks whether an obstacle covers less than 15% of the frame height and the current gait is walk or trot (not crawl — you can't step over things slowly with a crawl). If yes, the robot proceeds forward at reduced speed rather than turning, which is much more efficient for small objects on the floor.
Speed is continuously modulated based on the closest obstacle proximity: 0.3× at proximity > 0.8 (very close), scaling up to 1.0× when nothing is near. This produces natural deceleration as the robot approaches objects rather than a hard stop.
Servo Calibration
Getting eight servos to all have the same mechanical zero is the most tedious part of any quadruped build. Servos ship with manufacturing tolerances — a servo commanded to 0° might physically sit at 7° off-center. Multiply that across eight servos and the legs look different lengths even though they're identical hardware.
The calibration tool sends each servo to its zero position one at a time. You look at the physical leg, press + or - to add/subtract degrees until it looks right, and the tool writes the offsets to calibration_results.txt. These then go into config.py as the SERVO_OFFSETS dict, applied in software before every PWM conversion.
Notice all four knees are off by ~20°. That's consistent with a mounting issue — the knee servo horns were all attached one spline position clockwise of center. Correcting it in software is fine; it just means the IK is always working against a fixed offset at the hardware level.
What's Next
The trot gait works and the obstacle avoidance is functional on flat surfaces. The obvious next steps are:
- Terrain-adaptive step height — scale
STEP_HEIGHTbased on obstacle proximity rather than a fixed 40mm - IMU integration (MPU6050) for body tilt correction — the robot currently has no feedback on its actual orientation
- Replacing the Python IK solver with an on-device solution running directly on ESP32 #1 to reduce latency
- 3D-printed body enclosure — right now it's an exposed frame
73, Sunil