Home Dexter/dx/

Personal  ·  active

Dexter

fig.00 · Dexter

Dexter is my two-wheeled self balancing robot that I've been developing for a while. It initially started as a project to get the basics of full-stack robotics but evolved into developing useful engineering practices to design around system constraints and objectives, exploring simulation and design iteration.

fig.01 · driving a commanded square
fig.02 · holding balance

Dexter balances and navigates using a cascading control system allowing him to balance reliably using an LQR and be able to be remotely controlled to move around as well as autonomously navigate through 2d points using PD controllers.

The cascade is three loops. A navigation PD takes the current target point and produces a desired velocity and a desired heading. The velocity becomes the reference the LQR tracks while it balances, since the only way a two wheeled robot moves forward is by leaning and catching itself. The heading error goes to a second PD whose output is added to the LQR's wheel command at the mix, which gives differential steering, so turning never passes through the balancer at all.

A path is just a list of points. Dexter drives to each one and advances to the next once he is inside an arrival radius of it, so the square is four corners and the same controller running the whole way around.

Worth being precise about what he actually senses: tilt is an accelerometer and gyro fused in a Kalman filter, and heading is an integrated gyro rate. There are no encoders, so position is dead reckoned from the velocity the controller asked for rather than from anything measured.

Dexter's cascaded control system A waypoint list feeds a navigation PD, which produces a velocity command and a desired heading. A selector lets the remote replace both. The velocity command goes to the LQR, whose wheel command is combined at the mix with the heading PD's steering term and sent to the steppers. Tilt and heading are measured and fed back; position is dead reckoned from the commanded velocity. Waypoints list · arrival radius Navigation PD point → v, ψ* LQR 4-state · a = -Kx Mix v ± steer Steppers + robot no encoders Remote Heading PD ψ* - ψ → steer State estimate Kalman θ · gyro ψ x*, y* v_cmd gVel vL, vR ψ* steer IMU ψ θ, θ̇ x, y measured dead reckoned from the commanded velocity
fig.03 · the cascade. The steering term joins after the LQR, so the balancer never sees the turn.

Skills developed

Simulation for engineering decisions, hardware design, electrical design, control system design, etc.

I.

Prototype

This prototype was made as a first iteration to get experience designing the entire system, developing an understanding of the scope of the project, and how I could improve a first attempt using engineering methodology.

The first Dexter prototype: a purple 3D printed chassis on two wide printed wheels, with an ESP32 and a breadboarded CNC shield mounted inside.
The same prototype held in one hand, showing loops of jumper wire, cable ties and a breadboard packed into the chassis.
fig.04 · the original design, and the wiring behind it

I first sketched out what components and the wirings between them were required in order to balance the robot. This consisted of multiple failed attempts at wiring and powering everything through bread boards (and blowing up a couple components) until eventually I had figured everything out and learned the hard way not to do things. I played around with software attempting to figure out how to get accurate imu data using filtering and how to spin the motors. Once I was able to do that, I quickly 3D printed a couple different designs where I added features that I was missing one at a time until it was complete. I then wrote up a PID and spent over a week just trying to figure out how to tune it and get the robot to balance using deadbands, making sure the sensor data was right, mapping PID values to stepper motor values etc. Until eventually I was able to get it to balance, which is where I decided to then redo the entire project from more of an engineering perspective where I would be able to analyze the current systems limitations and pursue a more thought out version.

The prototype had a couple limitations in my eyes: the messy wiring was horrible to work with for debugging, moving around, and general unsight-liness. The control system had been tuned just by trial and error for an intuition based physical design and among other things a feeling that “this could be 10x better”. So I decided to focus on taking more of an engineer's perspective and actually model our system.

II.

System Modeling and Design Objective

Why Optimize Max Recovery Angle

Modeling our system allows us to do two big things: use a model based controller and simulate different physical designs to optimize decisions about mass distribution, height of the system, etc while keeping constraints like our motor actuation limits and objectives like maximizing max recovery angle.

The dominant behavior behind dexter is that of a translating inverted pendulum so to get the maximum amount of fidelity for the least amount of effort (as opposed to having to model wheel compliance, chassis flexibility, sensor noise, wheel slip, etc) I modeled him using the classical inverted cart pole system:

Derivation · plant
[1]
θ¨=gsinθcosθ(u+mpθ˙2sinθmc+mp)(43mpcos2θmc+mp)\ddot{\theta} = \frac{g\sin\theta - \cos\theta\left(\dfrac{u + m_p\,\ell\,\dot\theta^{2}\sin\theta}{m_c+m_p}\right)}{\ell\left(\tfrac{4}{3} - \dfrac{m_p\cos^{2}\theta}{m_c+m_p}\right)}

Where I started. The textbook cart-pole, θ measured from upright, driven by a force u pushing the cart along.

[2]
θ¨=mbgJ+rmbtips it over  θ    r(M+mb)+mbJ+rmbrights it  a\ddot{\theta} = \underbrace{\frac{m_b\,g\,\ell}{J + r\,m_b\,\ell}}_{\text{tips it over}}\;\theta \;-\; \underbrace{\frac{r\,(M+m_b) + m_b\,\ell}{J + r\,m_b\,\ell}}_{\text{rights it}}\;a

What I ended up simulating, linearized about upright. The input is a commanded wheel acceleration a rather than a force, and the motor stators are bolted to the frame so wheel torque pushes back on the body directly.

But I knew one of the limitations from the prototype was actually my motors, nema 17 stepper motors, weren't that strong and actually seemed to be slightly struggling. With just this dynamics model developing a control system would just allow me to balance and mathematically command whatever acceleration the model wants, but since I knew my motors would be something very specific to dexter and possibly a weak point, modeling them in order to get more accurate results for the control system in simulation was almost a necessity.

So two things had to change to get from the first line to the second, and both of them come from dexter being driven by steppers rather than by an idealized force. The first is the input. A stepper is commanded in velocity, not torque, so the honest input to the model is a wheel acceleration, and the torque becomes something the model solves for and then checks against what the motor can actually deliver. The second is the reaction. In a textbook cart-pole the force just pushes the cart along, but my motor bodies are bolted to the frame, so every bit of torque I put into a wheel pushes back on the chassis with no lever arm involved at all. That term is most of the righting authority at large lean angles, and leaving it out would have made the robot look far harder to save than it actually is.

The second line is also the one worth actually reading, because the whole battery argument later on is sitting inside it. The first term is what tips him over and it scales with m·g·ℓ, so every millimeter I move the center of mass up is torque the motors have to find from somewhere. The second term is what rights him, and it is the only thing I get to control. Everything in the design study is a fight between those two coefficients.

How the Simulation Works

The simulation is a small 2d python package rather than a full physics engine. I describe the body as a list of lumps, each one a mass at some height above the wheel axle with an optional vertical extent, and the sim collapses that list into the only three numbers the equations actually need: total body mass, where its center of mass sits, and its inertia about that center of mass. Putting the axle at the origin is what makes this easy to reason about, because a lump sitting at axle height adds mass but no tipping torque at all. Cart mass and pole mass stop being two categories and turn into a dial I turn by moving components up and down.

The equations of motion come out of a Lagrangian derivation done symbolically and then compiled down into scalar functions, since a single recovery run calls them hundreds of thousands of times. I also derived the same dynamics a second way with Newton-Euler free body diagrams and wrote a test that asserts the two agree, mostly because a sign error in the plant would quietly invalidate every design conclusion I drew afterwards. The state is position, velocity, tilt and tilt rate, and everything is integrated with fixed step RK4.

The part that makes the simulation worth anything is the motor model. Instead of letting the controller command whatever acceleration it wants, I gave the sim the pull-out torque curve for the steppers, digitized off the datasheet, which is the curve where the torque a stepper can actually hold collapses as it spins faster. The published curve is for 24 V and dexter runs on 12 V, so the sim reads it at twice the real rpm, which is a first order approximation until I measure the actual 12 V curve myself. The controller commands a wheel acceleration, the sim back-computes the torque that acceleration would need in the current state, and if that demand sits above the pull-out curve at the current wheel speed the command gets clipped and the run is flagged as a skip. A skip counts as outright failure rather than a penalty, because an open loop stepper that loses sync has also lost track of where its wheel is, so “it recovered eventually” is not really a recovery.

That gives me the two numbers the rest of the design hangs on, and both are worth defining properly. Fall time is not a simulated fall at all: it is one over the largest unstable eigenvalue of the open loop system, so it measures how much time the robot gives me before it is gone. Max recoverable angle is a bisection on the closed loop: lean the robot over to some angle, simulate forward, ask whether it came back upright without ever skipping a step, and binary search for the largest angle that survives. Every geometry in a sweep gets its LQR re-solved for that specific plant, so what I am comparing is designs against each other rather than one design against a controller that happened to suit it.

III.

Physical Design as a Control Design Problem

Battery Placement

One of the main questions that motivated Dexter's redesign was where to place the battery. During a prototype project review, I was asked whether moving the battery higher would make the robot easier to control. The common intuition is that taller inverted pendulums fall more slowly, similar to balancing a broomstick on your finger. However, that intuition only describes the passive falling behavior of the system. Dexter also has to actively recover using motors with limited torque and speed.

This made battery placement a control design problem rather than just a packaging decision. Moving the battery upward changes the center of mass and moment of inertia, which can increase the time available before the robot falls. But it also changes the amount of wheel torque and base acceleration required to recover from a disturbed state. A taller robot may fall more slowly, but if the motors saturate before the controller can bring the body upright, the extra fall time does not translate into better recoverability.

To answer this quantitatively, I swept battery height in the simulation while keeping the rest of the robot fixed and measured two competing metrics: passive fall time and maximum recovery angle.

Rather than optimizing fall time directly, I used maximum recovery angle as the primary objective, with fall time treated as a minimum constraint. This better matched the real design goal: the robot should not just fall slowly, it should be able to return upright from the largest possible disturbance before the motors saturate.

When I first ran this the battery was a 730 gram pack, and at that size it was around three quarters of everything the robot had to hold up. Battery height was not really a packaging decision at that point, it basically was the center of mass. The sweep found a real interior optimum: the best recovery came with the battery center of mass slightly below the center of mass of the rest of the robot, somewhere around 85 to 90% of that height. Going higher bought fall time and gave up recovery, and going all the way down took away the lever the wheels use to right the body in the first place. Both plots below come from that sweep, and the ridge of best battery heights running up the heat map is where the 0.9 comes from.

Then the reason for the big battery went away. That pack was only ever that large so it could power a jetson I had planned to put on top, and once I dropped that idea I could go down to a 157 gram 12v 2600mah pack. That changes the answer completely. The battery is now about a third of the body instead of three quarters, the rest of the robot dominates the center of mass, and the optimum stops being an interior point at all: in the sweep the best battery height pins itself to the bottom of the range for every robot height I care about. The answer collapsed into the boring one, which is to put it as low as it physically goes, and in the current design that is about 9 mm below the wheel axle where its weight pulls the body back upright instead of tipping it over.

That is still worth knowing rather than assuming. The battery is the heaviest single thing whose height I actually get to choose, since the motors are heavier at 280 grams each but bolted at the axle where the drivetrain puts them, so it is the one mass with real leverage over the design. The sweep also says something more useful than a single number: the good region is wide, and the battery can wander a fair way up before recovery starts falling off, which is the slack that let me put everything else where it needed to go.

Max recoverable tilt peaks at a base height around 0.12 m and falls off on both sides, while the open-loop fall-time constant rises steadily with height. Dashed lines mark ten control cycles at 100 and 200 Hz.
fig.05 · max recoverable angle vs fall time, from the 730 gram build. Taller buys reaction time and costs recovery angle, and the best recovery sits at an interior peak rather than at either extreme.
Heat map of max recoverable tilt over base height and battery height. The traced ridge of optimal battery height follows the battery equals 0.9 times base reference line.
fig.06 · robot height vs battery height, same build. The white ridge of best battery height sits almost exactly on the 0.9 × base line.

I also extended the sweep to compare different overall robot heights, which is the heat map above. Height is the parameter with real teeth. Recoverable angle climbs steeply out of the very short builds, peaks a few centimeters above the axle, and then falls away as the robot gets taller and the tipping torque the motors have to beat grows with it. Both extremes are bad for different reasons, and that is what pushed the redesign shorter rather than taller: the goal was not simply to make the robot taller, but to choose a geometry that maximized the recoverable envelope for the motors I actually had.

Battery height also turned out to be one of a handful of placement rules that matter more than the controller does. The other big one is the imu: it wants to sit as close to the axis of rotation as possible, because the further out it is the more of its own swing it reads as tilt. On the older and taller design with the big battery I had the imu mounted well above the axis and paid for it with vibrational oscillations that no amount of tuning really fixed. Where a part sits turned out to be worth more than anything I could do about it afterwards in software.

Wheel Size

The other physical knob worth simulating was wheel size, and it turns out to be the same problem wearing a different hat. A wheel is a lever between the motor and the ground: the force the robot can push against the floor with is the pull-out torque divided by the wheel radius, read off the motor curve at whatever rpm that ground speed implies. Small wheels turn the torque into a lot of force, but they have to spin fast to move the robot at all, so they run into the collapse in the pull-out curve early. Big wheels give up force to buy speed.

That tradeoff is easier to see than to describe. On 40 mm wheels dexter has something like 35 newtons available standing still and nothing left at all by 1.5 m/s. On 200 mm wheels he has about 6 and a half newtons, but he keeps them the whole way out. The 125 mm wheels I had been running sat in between without being particularly good at either end.

Available ground force against ground speed for five wheel diameters. Small wheels start high and collapse to zero at low speed, large wheels start low and stay flat.
fig.07 · the ground force each wheel size can actually deliver. The legend still marks 125 mm as current, which is what he was running when I did the study.

Sweeping wheel size against the recovery metrics put the answer at around 65 mm, which took survival against a sustained push from 64% to 83% while giving up almost nothing against a single hard shove. The part I did not expect is what wheel size actually couples to. It is not body mass, which barely moves the best diameter at all. It is center of mass height, which is the same parameter the battery study was about, so the two studies turn out to be the same design question asked twice.

This is also the point where max recoverable angle stopped being useful for ranking anything. The build had gotten bottom heavy enough that the search just ran to its ceiling on nearly every candidate wheel, so instead of a single angle I scored each design on how much of the whole disturbance space it survived. That is a recurring theme in this project: every metric I picked eventually stopped telling designs apart, and the useful move was noticing that rather than trusting the number.

IV.

Controller Design

This was another great point for having a system model: we were able to use model based controllers instead of just PID like the prototype. The goal was to not only use a new controller type, but then also design a new control system that allows us to have more behaviors like moving to a specific point controlled or staying balancing at a certain point as opposed to simply just balancing.

Derivation · controller
[1]
J=0 ⁣(xQx+Ra2)dt,Q=diag(1,1,1000,5),R=10J = \int_{0}^{\infty}\!\left(\mathbf{x}^{\top}Q\,\mathbf{x} + R\,a^{2}\right)dt, \qquad Q = \operatorname{diag}(1,\,1,\,1000,\,5), \quad R = 10

The cost I am actually minimizing, with the weights dexter runs. Tilt is worth a thousand times a millimeter of position error, and R prices the acceleration I spend buying that back.

[2]
a=Kx,K=R1BP,x=[xx˙θθ˙] ⁣a = -K\mathbf{x}, \qquad K = R^{-1}B^{\top}P, \qquad \mathbf{x} = \begin{bmatrix} x & \dot{x} & \theta & \dot{\theta} \end{bmatrix}^{\!\top}

Minimizing it gives one constant gain row multiplying the whole state: position, velocity, tilt and tilt rate.

[3]
Kx=Qx/RK_x = \sqrt{Q_x / R}

The one gain that does not care what the robot is built like. It falls out at 0.316 for my weights whether the helmet is on or off.

Two things about those weights are worth saying out loud. The first is that only the ratio between the tilt weight and R actually matters, because scaling both scales the cost without changing what minimizes it. My tuning sweep was a grid over both, but it was really a search along one line, and a tune at Q of 1000 with R of 10 is the same controller as Q of 100 with R of 1. The second is that the position weights are deliberately left at 1 while tilt sits at 1000. Position and velocity are dead reckoned from the acceleration I commanded rather than measured off the world, so leaning on them hard just feeds my own lag back into the robot. I tried raising the position weight to 50 once and it hunted worse, so it went back.

The nicest thing the math told me was something I had wrong. My instinct was that the velocity gain should be negative, since if he is rolling forward you would think you want to pull the wheels back. Solving it properly gives a positive coefficient, and wiring in the intuitive sign puts an unstable pole in the closed loop and makes him fall faster than doing nothing at all. I only caught it because I ran it in simulation before putting it on the robot.

LQR solves for the gain directly from the dynamics. The A and B matrices are built from the physical build (mass, COM height, wheel radius, and inertia), so the gain isn't a fixed knob: it's plant-dependent, and I re-tune it whenever the geometry changes.

The clearest example of that is the helmet. Dexter runs in two configurations, one with the top shell on and one without it for wired bring-up, and the shell adds around 170 grams high enough up to move the body center of mass from 50 mm off the ground to 80 mm. On 65 mm wheels that is the difference between sitting about 13 mm above the axle and about 40 mm above it, so the lever gravity gets to work with roughly triples. Same cost function, same Q and R, but the plant moved, so re-solving gave a tilt gain about 29% higher (13.48 against 17.41) and a tilt rate gain about half again as large. The position gain came out identical between the two, which is the tell that this is the plant changing and not me fiddling with knobs: with the state cost fixed that gain works out to the square root of Qx over R no matter what the robot weighs. Both sets live in the firmware and I switch between them over the serial link instead of reflashing.

The real constraint is the motors. The NEMA 17 steppers run open-loop in velocity mode, and their pull-out torque collapses as they spin faster, which is exactly the regime a recovery demands. If the controller commands a wheel acceleration that needs more torque than the motor can deliver at that speed, the stepper skips, loses sync, and the robot falls. That turns recovery into a torque race rather than a stability question: the gain that recovers the largest lean isn't the stiffest one, it's the gentlest one that still stays inside the pull-out envelope. An aggressive gain over-drives the steppers into skipping on even small disturbances.

There's a catch: a single instantaneous shove and a sustained, held push want opposite controllers. A soft gain survives the biggest impulsive kick but settles too slowly under a held push, overshooting on the way back and falling. A stiffer gain settles fast enough to ride out the held push but gives up some one-kick range. The two metrics, one-kick ROA and pulse ROA, are in direct conflict, so I tuned a Q/R that did well against both rather than maxing out either.

From the sizing sim I sized all of this in a 2D simulation before committing to hardware, sweeping controller gains and physical parameters (battery height, mass, wheel radius) for region-of-attraction, not just stability. The recurring lesson: the boundary of what the robot can recover tracks the stepper pull-out curve, so the physical design sets a ceiling on robustness that no controller can beat.

So the LQR controller is acting as the main balancing controller which commands the stepper motors. However, in order to allow us to have a behavior where we can command Dexter to a point (navigation) we have to add some cascading structure. We can use a PD controller that takes in some reference point and outputs both a desired velocity and a heading direction based on the current state. The desired velocity is what we give the LQR controller but the reference heading direction gets sent to another PD controller for differential steering. What this means is that before commanding the LQR outputs to the stepper motors, we combine its result along with the heading PD to give us differential steering that allows us to rotate the two wheeled system towards our desired point.

V.

Electrical System & PCB

Between prototype and redesign the electrical system has not changed all that much so far. Currently, an esp32 is the main brain of the system, running the entire control loop at 400 Hz. It uses an MPU 6050 and fuses its accelerometer and gyroscope data using a two state Kalman filter that estimates tilt and gyro bias together. The esp32 then sends the control systems motor commands using two DRV 8825 motor drivers and nema 17 stepper motors. The battery is a 12v 2600mah pack at 157 grams, which powers the motors and the esp32 logic locally and is small enough to sit under the axle.

The main goal with this redesign was to get rid of the horrible mess of wiring and make weighting each component just a little bit easier so as to not have to account for different breadboards, wires, etc. So I essentially designed a carrier board for the aforementioned electronics, tracing all of the connections and having header pins wherever available for easy removal of components. It also features capacitors to account for motor noise, thicker traces for power related ones and thinner ones for logic.

The populated carrier board: a red PCB carrying an ESP32, an MPU 6050, two DRV 8825 drivers on heatsinks, two electrolytic capacitors and two screw terminal blocks.
The same board in the KiCad 3D viewer, showing the module footprints, header pins and both capacitors on a bare green board.
fig.08 · real life PCB alongside a 3D model of it
ComponentDetailQty
Computeesp32 · main brain, full control loop1
IMUMPU 6050 · accelerometer + gyroscope fusion1
Motor driversDRV 88252
Motorsnema 17 stepper2
Battery12v · 2600mah · 157 g, mounted below the axle1

The chassis around all of this is mostly in service of the two studies earlier on. It is built around the height the simulation picked rather than whatever was convenient to print, and everything inside it has its own fitted compartment so each part sits in a known place and can come out without disturbing anything around it. The battery box is the clearest example of that, slung under the axle where the sweep wanted the mass to be. The other thing that mattered more than I expected was how it is all held together. I had been using nuts and bolts, and moving to heat set inserts made the whole structure noticeably more rigid, which took a lot of the vibration out of what the imu was reading. The helmet goes on last, and it is the reason there are two controller tunes instead of one.

CAD view of Dexter with the helmet off, showing the backplate, the motor mounts and the battery compartment slung below the wheel axle.
The same CAD model with the helmet fitted, a vented shell closing over the electronics with the logo cut into the face.
fig.09 · the build in CAD, helmet off and helmet on