devol.dev

Mecanum drive

A conventional wheel pushes in exactly one direction. A mecanum wheel cannot even manage that on its own: its rollers sit at 45 degrees to the hub, so it drives along one axis and rolls freely, with no resistance at all, along the perpendicular one. On its own that is a worse wheel.

Four of them, mounted so the roller angles alternate around the chassis, add up to something no ordinary wheel can do. The robot translates in any direction and spins independently of where it is going.

3.66 m floor, 0.42 m chassis

Drive it

Click the field, then drive. W A S D translates, Q and E spin. The four readouts are motor powers, from -1 to 1: the numbers your code would be writing this instant.

FL0.00
FR0.00
BL0.00
BR0.00
Click the field to take the controls.

Hold A or D and watch the wheels rather than the robot. Two turn one way, two turn the other, and the chassis slides sideways with nothing on it pointing sideways. That is the whole trick, and the rest of this page is why it works.

Three degrees of freedom, four wheels

A wheel at body-frame offset (px,py)(p_x, p_y) on a chassis moving with velocity (vx,vy)(v_x, v_y) and yaw rate ω\omega has a contact point moving at

vcontact=(vxωpy, vy+ωpx)v_{contact} = (v_x - \omega p_y,\ v_y + \omega p_x)

A mecanum wheel is driven along one axis only. Call its handedness s=±1s = \pm 1, depending on which 45 degrees the rollers sit at. Projecting the contact velocity onto that driven axis gives one equation per wheel:

vi=vxsivyω(py,i+sipx,i)v_i = v_x - s_i v_y - \omega (p_{y,i} + s_i p_{x,i})

Mount the wheels so that the diagonals share a handedness, which is s=+1s = +1 for front-left and back-right and s=1s = -1 for the other two, and those four equations become the ones every mecanum robot runs on:

FL=vxvy(L+W)ωFR=vx+vy+(L+W)ωBL=vx+vy(L+W)ωBR=vxvy+(L+W)ω\begin{aligned} FL &= v_x - v_y - (L+W)\omega \\ FR &= v_x + v_y + (L+W)\omega \\ BL &= v_x + v_y - (L+W)\omega \\ BR &= v_x - v_y + (L+W)\omega \end{aligned}

where LL is half the wheelbase and WW is half the track width.

Three unknowns, four equations. The chassis has three degrees of freedom and four motors, which means the wheel speeds are not independent: pick any three and the fourth is already decided. That redundancy is worth remembering when a robot behaves oddly and only one wheel looks wrong.

The diagonal that idles half the drivetrain

Set vx=vyv_x = v_y, which is a 45 degree command, and two of those equations collapse:

FL=0BR=0FR=BL=2vxFL = 0 \qquad BR = 0 \qquad FR = BL = 2v_x

Hold W and A together in the tool above. Two readouts sit at exactly zero and the other two peg at 1.00. The robot drives at 45 degrees on half its drivetrain, and it looks like a fault the first time you see it.

Watch the scale factor while you do it. A diagonal asks the two working wheels for 2\sqrt{2} times what a straight line asks, so the command comes back scaled to 70.7%. That number turns up again in the next section, and for the same reason.

Why it goes sideways

The equations say what the wheels must do. They do not say why it works.

For that, stop thinking about velocities and look at forces. A mecanum wheel can only push along its roller axis, at 45 degrees. Four wheels, four diagonal pushes, and the components nobody wants cancel out between them.

Contact patch forces

Four diagonal pushes

The dashed lines are the axis each wheel can push along, visible before you touch anything: four diagonals, alternating. Drive, and a thin arrow grows along each one, scaled by motor power. The heavy arrow from the centre is their sum.

FL0.00
FR0.00
BL0.00
BR0.00
Click the field to take the controls.

Drive straight forward and all four wheels push diagonally outward. Two push forward-right, two push forward-left, the lateral halves cancel, and what is left is pure forward thrust at 2/2\sqrt{2}/2 of what the motors could have given you along their own axes.

Now strafe. Front-left and back-right reverse, so their diagonal pushes flip. The forward components cancel instead of the sideways ones, and the sum points cleanly to the side. Nothing on the robot points that way. The sideways motion is four wheels fighting each other in a specific, calculated proportion.

That 2/2\sqrt{2}/2 is also the honest answer to why mecanum feels weak. Every wheel spends part of its effort pushing against another wheel.

Field-centric driving

Everything so far is in the robot’s frame. Forward means whatever direction the robot’s nose happens to point.

Operators do not think that way. Someone driving a platform watches it from a fixed vantage point and thinks in the world frame, and the moment the chassis yaws 180 degrees, robot-centric controls invert and become unusable.

The fix is one rotation applied to the stick before the kinematics ever see it:

[vxvy]robot=[cos(θ)sin(θ)sin(θ)cos(θ)][vxvy]field\begin{bmatrix} v_x \\ v_y \end{bmatrix}_{robot} = \begin{bmatrix} \cos(-\theta) & -\sin(-\theta) \\ \sin(-\theta) & \cos(-\theta) \end{bmatrix} \begin{bmatrix} v_x \\ v_y \end{bmatrix}_{field}

Yaw is left alone. Spinning means the same thing in either frame.

Whose forward is it

Field-centric versus robot-centric

This chassis starts already yawed 90 degrees, as it would be after any turn. Hold W. Robot-centric sends it along its own nose. Tick field-centric and W means the same world direction it did before the turn.

FL0.00
FR0.00
BL0.00
BR0.00
Click the field to take the controls.

Hold W with the box unchecked, then check it and hold W again. Same key, two different directions, and only one of them is what the operator meant.

This needs a heading, so it needs a working IMU and a zeroing routine. It is the single highest-value thing you can add to a mecanum drivetrain, and it is about four lines.

What to do when the numbers exceed one

The equations happily return 2.4. A motor cannot.

Divide every wheel by the largest magnitude, but only when that largest magnitude is over the limit:

scale=1max(1, maxivi)\text{scale} = \frac{1}{\max(1,\ \max_i |v_i|)}

Scaling all four together preserves the ratios between them, and the ratios are the direction of travel. Scale only the wheel that is over the limit and the chassis drives somewhere nobody asked for, which is a genuinely confusing bug because it only appears at high speed and in combined moves.

Hold W and Q together in the first tool and watch the status line. Full forward plus full spin is more than the motors have.

Mounting them wrong

Mecanum wheels come in left and right handed pairs. Four wheels with two handednesses is sixteen ways to bolt them on, and one is right. The wrong ones do not all fail alike, which is why the selector below offers three of them rather than a single “wrong”.

This one starts on a broken build.

Three ways to get it wrong

Roller handedness

Drive it, then change the mounting and drive again. The grey arrow is what you asked for, the rust arrow is what the chassis can actually deliver. On a correct build they sit exactly on top of each other.

FL0.00
FR0.00
BL0.00
BR0.00
Click the field to take the controls.

Rollers form an O. Every wheel the opposite handedness from correct. Look down at it and the roller axes make an O rather than an X. Each yaw term now scales with (LW)(L-W) instead of (L+W)(L+W), and on a square chassis that is exactly zero:

FL=FR=BL=BR=vx±vy+0ωFL = FR = BL = BR = v_x \pm v_y + 0 \cdot \omega

The robot translates in any direction you like and cannot turn at all. Hold Q or E and every motor reads zero. That is the cruel one, because driving and strafing are perfect, so the build looks right until the first time somebody tries to rotate.

Make the chassis oblong and yaw comes back, feebly, in the ratio LW/(L+W)|L-W| / (L+W). A 300 by 400 robot turns at one seventh of the authority it should have, which presents as a sluggish yaw response rather than as a mounting fault, and sends people tuning gains that were never the problem.

All four the same handedness. Every equation now reads vi=vxvy()ωv_i = v_x - v_y - (\dots)\omega. The vyv_y column is exactly the negative of the vxv_x column, so the matrix drops from rank 3 to rank 2, and the chassis has lost a degree of freedom outright.

The lost direction is the 45 degree diagonal vx=vyv_x = v_y. Command it and every motor gets a zero: the robot does nothing. Command a pure strafe and you get

(vx,vy)=(0,1)(12,12)(v_x, v_y) = (0, 1) \quad \longrightarrow \quad \left(-\tfrac{1}{2}, \tfrac{1}{2}\right)

so the chassis sets off at 135 degrees to its nose while the stick is held straight to the left. It is not slow or weak. There is a whole direction its wheels cannot drive, and nothing they do resists being shoved along it either.

One axle swapped. This stays rank 3, which makes it the hardest to diagnose: the robot moves in every direction, just not by the amounts you asked for. The yaw term on the swapped axle picks up (LW)(L-W), so on a square chassis those two wheels contribute exactly nothing to rotation. The robot turns on one axle while the other rides along.

There is no separate entry for swapping the front axle instead of the back. Relabel a back-swapped chassis under a 180 degree rotation, so FL trades with BR and FR with BL, and you have the front-swapped one exactly. They are one machine seen from either end, and the tests check that they agree to the last bit. Which axle is “wrong” depends only on which end you painted the front on.

The rule that falls out of all this: looking down at the robot, the roller axes should form an X. Anything else and the algebra above is describing a different machine than the one on your field.

Why every snippet you find has different signs

This page uses xx forward, yy left, and counter-clockwise positive, which is the standard mobile robotics convention. Most teleoperation code does not.

A gamepad hands you left_stick_y inverted, left_stick_x positive to the right, and right_stick_x positive clockwise. Written in those variables, the substitution is vx=yv_x = y, vy=xv_y = -x, and ω=rx\omega = -rx. Put that through the four equations and every sign flips into the arrangement you have seen a hundred times:

FL=y+x+rxFR=yxrxBL=yx+rxBR=y+xrx\begin{aligned} FL &= y + x + rx \qquad & FR &= y - x - rx \\ BL &= y - x + rx \qquad & BR &= y + x - rx \end{aligned}

Same equations. Different frame. This is the entire reason four snippets from four sources give four different results, and it is why copying one without knowing its convention is a coin flip.

Derive it once in whatever frame you like, then be consistent.

The code

Robot-centric, which is where to start. The stick names below are one common gamepad API’s; every other one hands you the same three axes under different names, and the algebra does not care.

In Java:

// Forward, left, and counter-clockwise, from the sticks.
double y  = -gamepad1.left_stick_y;   // stick y is inverted
double x  =  gamepad1.left_stick_x;
double rx =  gamepad1.right_stick_x;

double fl = y + x + rx;
double fr = y - x - rx;
double bl = y - x + rx;
double br = y + x - rx;

// Scale all four together, but only if one of them is over the limit.
double peak = Math.max(1.0, Math.max(
    Math.max(Math.abs(fl), Math.abs(fr)),
    Math.max(Math.abs(bl), Math.abs(br))));

frontLeft.setPower(fl / peak);
frontRight.setPower(fr / peak);
backLeft.setPower(bl / peak);
backRight.setPower(br / peak);

The same in C++:

// Forward, left, and counter-clockwise, from the sticks.
const double y  = -gamepad.left_stick_y;   // stick y is inverted
const double x  =  gamepad.left_stick_x;
const double rx =  gamepad.right_stick_x;

const double fl = y + x + rx;
const double fr = y - x - rx;
const double bl = y - x + rx;
const double br = y + x - rx;

// Scale all four together, but only if one of them is over the limit.
const double peak = std::max({1.0, std::abs(fl), std::abs(fr),
                              std::abs(bl), std::abs(br)});

frontLeft.setPower(fl / peak);
frontRight.setPower(fr / peak);
backLeft.setPower(bl / peak);
backRight.setPower(br / peak);

Field-centric is the same eight lines with one rotation in front of them.

In Java:

// Heading from the IMU, in radians, counter-clockwise positive, zeroed
// at whichever world direction the operator treats as forward.
double h = heading();

double rotX = x * Math.cos(-h) - y * Math.sin(-h);
double rotY = x * Math.sin(-h) + y * Math.cos(-h);

double fl = rotY + rotX + rx;
double fr = rotY - rotX - rx;
double bl = rotY - rotX + rx;
double br = rotY + rotX - rx;

The same in C++:

// Heading from the IMU, in radians, counter-clockwise positive, zeroed
// at whichever world direction the operator treats as forward.
const double h = heading();

const double rotX = x * std::cos(-h) - y * std::sin(-h);
const double rotY = x * std::sin(-h) + y * std::cos(-h);

const double fl = rotY + rotX + rx;
const double fr = rotY - rotX - rx;
const double bl = rotY - rotX + rx;
const double br = rotY + rotX - rx;

Two notes on the difference between this and the equations above.

The (L+W)(L+W) factor is gone. Teleoperation code folds it into how hard rx is scaled, because an operator is not commanding a yaw rate in radians per second, they are commanding “turn about this much”. The physics is unchanged, the units are just informal. Anything closed-loop wants the factor back, in real units.

And peak here uses the largest wheel magnitude. Many snippets use abs(y) + abs(x) + abs(rx) instead, which is an upper bound on the same thing. It always keeps you in range, and it is slightly conservative: it can scale you down when nothing needed scaling.

What mecanum costs you

Holonomic motion is not free, and mecanum is often specified without anyone pricing it.

Roughly 30% of the thrust is gone. That 2/2\sqrt{2}/2 is not recoverable. Each wheel spends part of its force cancelling another wheel’s, and that is the mechanism working correctly, not a loss you can tune out.

It cannot resist a lateral push. Rollers spin freely under sideways load, which is precisely the property that makes strafing possible. The same compliance shows up as poor disturbance rejection on a slope, on an uneven floor, or against any external contact.

Traction varies as the wheel turns. One roller carries the load at a time, so the contact patch and available friction change through a revolution. Worn rollers and dust both make it worse, and the variation is periodic, which control loops find harder than a constant.

Dead reckoning degrades. Slip is designed into the mechanism, so wheel encoders disagree with the actual pose more than they would on traction wheels. Where pose accuracy matters, the usual answer is unpowered tracking wheels that carry no load and therefore no slip.

Mecanum buys omnidirectional motion, which is genuinely valuable when a platform has to align in a constrained space without room to turn. It pays for it in thrust, disturbance rejection, and odometry. Whether that is a good trade depends entirely on what the platform has to do.

Handedness as data

The kinematics live in one module with no rendering in it, the same arrangement as the arm and the PID loop. Handedness is data rather than hardcoded signs, which is the only reason the broken mountings above are computable at all: the Jacobian is built per configuration, and its rank, null space, and pseudoinverse fall out of the same code path that serves the correct one.

The tests check properties rather than arithmetic. Wheel speeds and body velocity round-trip exactly. Strafing drives the two diagonals in opposite directions. The four roller forces sum to pure sideways thrust with zero forward component. An all-same build has rank 2, its null space is the 45 degree diagonal, and commanding that direction turns no wheel at all. An O-pattern build has four yaw terms that vanish on a square chassis and recover on an oblong one in exactly the ratio LW/(L+W)|L-W|/(L+W). A front-swapped axle and a back-swapped axle agree to within 101210^{-12} once one of them is turned around. The pose integrator is closed form, so chopping a two second arc into four steps and into four thousand gives the same answer to nine decimal places.