A holonomic robot is a platform that can independently control its movements in the plane within the limits of its actuators.
Today we are going to break that limitation. We are going to build a holonomic robot.
A holonomic system is one that can independently control all its degrees of freedom. In plain English: it can move in any direction in the plane (X, Y) and rotate on itself simultaneously and independently. It can go “sideways” without turning its nose, or move forward while spinning on its axis.
To achieve this, we will use the famous Omni Wheels.
Omni vs Mecanum: Although both allow lateral movement, they are different.
- Omni: Have small rollers perpendicular (90º) to the main axis.
- Mecanum: Have rollers at 45º. Today we will focus on Omni, using a 3-wheel triangular configuration (known as Kiwi Drive).
Vectors at 120 Degrees
The secret of this robot is not in complex code, but in trigonometry.
We have 3 motors arranged in an equilateral triangle, separated 120º from each other. When we want to move the robot in a direction (movement vector), we need to decompose that force into the specific direction each wheel points.
If we define:
: Desired linear speed. : Desired movement angle. : Angular rotation speed (spin on itself).
The speed (
(These angles depend on how you define the robot’s front and the positive direction of each motor, but the 120° separation remains.)
The beauty of this formula is that, when adding the force vectors of the three wheels, the unwanted components cancel each other out. The result is clean movement in the chosen direction.
Components
- [1x|blue] Arduino Uno | Or Nano
- [3x|green] Continuous Rotation Servos | FS90R or hacked MG996R
- [3x|orange] Omni Wheels | Compatible with the servo shaft
- [1x|gray] Circular or triangular chassis | 3D print or acrylic
- [1x|red] Regulated power source or battery | Compatible voltage and sufficient current for all three servos
- Why servos? Because they integrate the gearbox and driver. They are controlled with a single signal wire without needing an L298N or motor shields.
Wiring Diagram
When using continuous servos, we only need three digital pins for the control signals. The Servo library generates the pulses and does not require PWM pins.
Servo Signals
Each servo receives an independent signal from the Arduino.
[ { “from”: “Servo 1 (Front Left) signal”, “to”: “Pin 9”, “color”: “green” }, { “from”: “Servo 2 (Front Right) signal”, “to”: “Pin 10”, “color”: “green” }, { “from”: “Servo 3 (Rear) signal”, “to”: “Pin 11”, “color”: “green” } ]
Servo Power
Servos should not be powered from the Arduino’s 5V pin. Use a suitable battery or power supply and share GND.
[ { “from”: “Servos VCC (all, red)”, “to”: “Battery (+)”, “color”: “red”, “note”: “NOT to Arduino” }, { “from”: “Servos GND (all, black)”, “to”: “Battery (-) + Arduino GND”, “color”: “black”, “note”: “Mandatory common ground” } ]
The Code
We will implement a moveRobot(angle, speed, rotation) function that encapsulates all the trigonometry.
For continuous rotation servos:
- Signal 1500 (or 90º) = Stop.
- Signal 2000 (or 180º) = Maximum speed in one direction.
- Signal 1000 (or 0º) = Maximum speed in the opposite direction.
We must map the result of our formulas (a number between -1 and 1) to these pulse values.
Definitions and setup
#include <Servo.h>
Servo wheel1; // Front Left
Servo wheel2; // Front Right
Servo wheel3; // Rear
// Pins
const int PIN_S1 = 9;
const int PIN_S2 = 10;
const int PIN_S3 = 11;
// Constant to convert degrees to radians
const float DEG_TO_RAD = 0.01745329251;
void setup() {
Serial.begin(9600);
wheel1.attach(PIN_S1);
wheel2.attach(PIN_S2);
wheel3.attach(PIN_S3);
stopRobot();
delay(2000);
}
Mathematical Core
This function receives where we want to go (angle in degrees), how fast (0.0 to 1.0), and if we want to rotate while doing so.
void moveOmni(float angleDegrees, float speed, float rotation) {
// Convert angle to radians
float angleRad = angleDegrees * DEG_TO_RAD;
// --- KINEMATIC FORMULAS (Kiwi Drive) ---
// Calculate the power needed for each motor
// The layout assumes wheel 3 is at the rear (270º), wheel 2 at 30º, wheel 1 at 150º
float v1 = speed * cos(angleRad - (150 * DEG_TO_RAD)) + rotation;
float v2 = speed * cos(angleRad - (30 * DEG_TO_RAD)) + rotation;
float v3 = speed * cos(angleRad - (270 * DEG_TO_RAD)) + rotation;
// --- NORMALIZATION ---
// If we ask for high speed and high rotation, the sum might exceed 1.0
// We find the maximum value to scale everything proportionally
float maxVal = max(abs(v1), max(abs(v2), abs(v3)));
if (maxVal > 1.0) {
v1 /= maxVal;
v2 /= maxVal;
v3 /= maxVal;
}
// --- SEND TO MOTORS ---
// Map from (-1.0, 1.0) to (1000, 2000) microseconds
// 1500 is stopped.
writeServo(wheel1, v1);
writeServo(wheel2, v2);
writeServo(wheel3, v3);
}
// Helper function to translate normalized speed to a servo pulse
void writeServo(Servo &s, float val) {
// Adjust these values if your servos spin in reverse
int microseconds = 1500 + (val * 500);
s.writeMicroseconds(microseconds);
}
void stopRobot() {
wheel1.writeMicroseconds(1500);
wheel2.writeMicroseconds(1500);
wheel3.writeMicroseconds(1500);
}
Test loop
To test it, we’ll make the robot “dance”: it will move laterally (Strafe) without turning, and then rotate.
void loop() {
// 1. Move forward (0 degrees)
moveOmni(0, 0.5, 0);
delay(1000);
// 2. Move to the right (90 degrees) WITHOUT turning the chassis
// This is where you see the Omni effect
moveOmni(90, 0.5, 0);
delay(1000);
// 3. Move backward (180 degrees)
moveOmni(180, 0.5, 0);
delay(1000);
// 4. Move to the left (270 degrees)
moveOmni(270, 0.5, 0);
delay(1000);
// 5. Spin: Rotate on its axis
moveOmni(0, 0, 0.5);
delay(2000);
stopRobot();
delay(2000);
}
Understanding the Movement
If you analyze the code and the physical behavior, you’ll see something curious: When you ask it to go right (90º), the rear wheel (Wheel 3) pushes, but the front wheels (1 and 2) spin in opposite directions to cancel forward motion and generate only lateral displacement.
It’s a real-time vector force choreography.
Challenges and Calibration
This robot is very sensitive to mechanics:
- Center of Gravity: If the weight is not centered in the triangle, one wheel will slip more than the others and the robot won’t go straight.
- Surface: Omni wheels need a hard, smooth surface. They work poorly on carpets because the small rollers get stuck.
- Servo Calibration: The 1500 µs point doesn’t always coincide with a perfect stop. You’ll need to adjust the neutral point of each unit and possibly apply different speed factors.
Continuous rotation servos are controlled by speed, not position, and two units do not respond exactly the same. This setup is useful for experimenting with kinematics, but for precise trajectory, motors with encoders and closed-loop speed control are preferable.
Next Step
We have mastered the 3 axes at 120º. But what if we want more traction and stability? That’s where 4 wheels and the most popular configuration in the industry come in: Mecanum Wheels.
But that… we will see in the next tutorial.