Skip to content

State estimation ​

A robot never measures its state directly; it infers orientation, velocity and position from noisy, biased sensors. This page covers IMU models, complementary and Mahony filters, the Kalman filter, leg odometry and LiDAR SLAM, and what Strido actually publishes.

In Strido

The XIAO ESP32-S3 reads an LSM6DSOX IMU and runs a Mahony filter in its 200Hz control loop (firmware/strido_esp32s3/src/main.cpp, period kControlPeriodUs = 5000 in board_config.h). ros2/strido_control/strido_control/mcu_bridge.py republishes it as /imu/data; gait_node.py publishes /odom by dead reckoning; an RPLIDAR C1 publishes /scan (see docs/software.md and Sensors).

IMU measurement models ​

An inertial measurement unit contains a 3-axis gyroscope (angular velocity) and a 3-axis accelerometer (specific force). Both are expressed in the sensor's body frame {b}.

Gyroscope.

ω~=ω+bg+ng

bg is a slowly drifting bias (temperature dependent), ng white noise. Integrating the gyro gives orientation, but integrating the bias gives an angle error growing linearly in time: a bias of only 0.5∘/s becomes 30∘ after a minute.

Accelerometer. It measures acceleration minus gravity, rotated into the body:

a~=R⊤(p¨−g)+ba+na,g=(0,0,−9.81)⊤m/s2

where R rotates body to world. Standing still (p¨=0), it reads R⊤(0,0,9.81)⊤: the "up" vector in body coordinates. That fixes roll and pitch with no drift, but it is noisy and corrupted whenever the body accelerates (every footstep). It says nothing about yaw, because rotating about gravity leaves the gravity vector unchanged.

The two sensors are complementary: gyro is smooth but drifts; accelerometer is noisy but drift-free.

The complementary filter ​

For one tilt angle θ, blend the integrated gyro (trusted at high frequency) with the accelerometer angle (trusted at low frequency):

θ^k=α(θ^k−1+ω~kΔt)+(1−α)θacc,k,θacc=atan2(a~y,a~z)

In the frequency domain this is a high-pass on the gyro plus a low-pass on the accelerometer with the same cut-off; the crossover time constant is

τ=αΔt1−α

Worked example. At Strido's Δt=5ms, choosing α=0.98 gives τ=0.98×0.005/0.02≈0.25s: longer than the 0.2s stance of the trot, so footstep jolts are mostly filtered out while gyro drift is corrected within about a second.

The Mahony filter (what the firmware runs) ​

Mahony's filter is a complementary filter on the rotation group, using a quaternion q^ instead of separate angles. Each step:

  1. Predict the gravity ("up") direction from the current estimate: v^=R^⊤ez.
  2. Normalise the accelerometer: a¯=a~/‖a~‖.
  3. The error is the rotation needed to align them:
e=a¯×v^
  1. Correct the gyro with proportional and integral feedback (the integral term converges to minus the gyro bias, cancelling it):
bk=bk−1+KieΔt,ωc=ω~+Kpe+bk
  1. Integrate the quaternion kinematics q˙=12q⊗(0,ωc) and renormalise:
q^k=q^k−1+12q^k−1⊗(0,ωc)Δt‖⋅‖

Kp sets the crossover (roughly τ≈1/Kp); Ki removes constant gyro bias. Yaw is not observable without a magnetometer, so it drifts.

In the firmware, Adafruit_Mahony is started at 1/Δt=200Hz and fed through updateIMU (gyro and accelerometer only, no magnetometer). Before filtering, board axes are remapped to the base frame because the IMU is mounted with its x to robot left and y to the rear:

(ωx,ωy,ωz)=(−ω~y,ω~x,ω~z),(ax,ay,az)=(−a~y,a~x,a~z)

The sensor is configured for ±8g, ±1000∘/s at 416Hz output data rate. The repository keeps the library's default gains (Adafruit AHRS: Kp=0.5, Ki=0, i.e. no bias integration).

The Kalman filter ​

The Kalman filter is the optimal estimator for linear systems with Gaussian noise. State x with covariance P, model

xk=Fxk−1+Buk+wk,w∼N(0,Q);zk=Hxk+vk,v∼N(0,Rn)

Predict (propagate the model; uncertainty grows):

x^k∣k−1=Fx^k−1+Buk,Pk∣k−1=FPk−1F⊤+Q

Update (fuse a measurement; uncertainty shrinks):

Kk=Pk∣k−1H⊤(HPk∣k−1H⊤+Rn)−1x^k=x^k∣k−1+Kk(zk−Hx^k∣k−1),Pk=(I−KkH)Pk∣k−1

The gain K weighs the innovation z−Hx^ by how much we trust the measurement (Rn) versus the prediction (P). A tilt filter with state (θ,bg), gyro as input and accelerometer angle as measurement converges to a complementary filter with an automatically tuned α and bias estimate. Nonlinear systems (rotations, legged robots) use the extended Kalman filter, which linearises F and H about the current estimate.

Leg odometry ​

If a stance foot does not slip, its world position is fixed, so the body velocity follows from the leg's joint velocities through forward kinematics (see Kinematics):

vbody=−(J(q)q˙+ω×pfoot)

averaged over the stance feet and integrated for position. Errors come from slip, contact detection and calibration, so it drifts; state-of-the-art quadrupeds fuse it with the IMU in an EKF.

Strido's hobby servos have no position feedback: /joint_states reports the commanded angles after clamping, not measured ones. True leg odometry is therefore not available, and gait_node.py does something simpler: it integrates the commanded body velocity,

xk=xk−1+(vxcos⁡ψ−vysin⁡ψ)Δt,yk=yk−1+(vxsin⁡ψ+vycos⁡ψ)Δt,ψk=ψk−1+ωzΔt

with no slip model. If the robot actually achieves 80 % of a 0.30m/s command, /odom overestimates distance by 0.06m every second.

LiDAR scan matching and SLAM ​

A 2D LiDAR returns ranges rj at bearings θj. Scan matching finds the rigid transform (R,t) that best aligns a new scan to a previous scan or map, e.g. with ICP (iterative closest point):

(R⋆,t⋆)=arg⁡minR,t∑j‖Rpj+t−mc(j)‖2

where mc(j) is the closest map point to pj. Chaining matches gives LiDAR odometry, which also drifts. SLAM (simultaneous localisation and mapping) removes the drift by recognising previously seen places (loop closure) and solving a pose-graph optimisation over all poses at once. Wheel/leg odometry is a useful initial guess for the matcher, which is the role the code comment assigns to Strido's /odom ("a seed for SLAM").

What Strido publishes ​

TopicContentsSource
/imu/data (sensor_msgs/Imu, frame imu_link, axes aligned with base_link)orientation quaternion from the Mahony filter, angular velocity in rad/s, linear acceleration in m/s2 (gravity included); not published while the IMU reports a faultmcu_bridge.py, state stream at 100Hz (kStatePeriodMs = 10)
/odom + TF odom → base_link (nav_msgs/Odometry)dead reckoning of the commanded velocitygait_node.py only
/scan (sensor_msgs/LaserScan, lidar_link)RPLIDAR C1 ranges at 10Hzsllidar_ros2

The learned policy uses only /imu/data: the gyro and the gravity direction derived from the quaternion (see Reinforcement learning).

Check yourself ​

  1. Why can an accelerometer give roll and pitch but not yaw?

    Answer

    It measures the gravity direction in the body frame; rotating about the vertical axis does not change that direction.

  2. With Δt=5ms, what α gives a complementary-filter time constant of 0.5s?

    Answer

    α/(1−α)=100, so α=100/101≈0.990.

  3. What does the integral gain Ki in the Mahony filter estimate, and what happens with Ki=0?

    Answer

    The gyro bias. With Ki=0 a constant bias leaves a small steady roll/pitch offset of about bg/Kp and uncorrected yaw drift.

  4. In the Kalman update, what happens to K as Rn→∞?

    Answer

    K→0: the measurement is ignored and the estimate follows the prediction.

  5. Why can't Strido do classic leg odometry?

    Answer

    Its servos give no joint-position feedback, so the actual q and q˙ are unknown; only commands are available.

Further reading ​

  • R. Mahony, T. Hamel and J.-M. Pflimlin, "Nonlinear Complementary Filters on the Special Orthogonal Group", IEEE Transactions on Automatic Control, 2008.
  • S. Thrun, W. Burgard and D. Fox, Probabilistic Robotics, MIT Press, 2005.
  • J. Solà, "Quaternion kinematics for the error-state Kalman filter", arXiv:1711.02508, 2017.
  • M. Bloesch et al., "State Estimation for Legged Robots — Consistent Fusion of Leg Kinematics and IMU", Robotics: Science and Systems, 2012.
  • W. Hess, D. Kohler, H. Rapp and D. Andor, "Real-Time Loop Closure in 2D LIDAR SLAM" (Cartographer), IEEE ICRA, 2016.

MIT licence. Mechanical design derived from Yertle by Jerome Graves. CAD, simulation and native tests are not hardware evidence — see Verification.