Introduction
In my previous articles, I've built a complete GNSS positioning engine. But for robust autonomous navigation, we need more than satellites... We need an Inertial Measurement Unit (IMU) that works when GNSS signals drop. However, testing fusion algorithms with real hardware is expensive and slow. So the best solution is to build a high-fidelity IMU simulator.
This article is the process of how to implement an IMU simulator from first principles, breaking down the problem into four logical levels of increasing complexity.
🎯 LEVEL 1: IMU Fundamentals - More Than Just Raw Data
An IMU doesn't measure what you might intuitively think. Understanding this distinction is crucial for anyone working with inertial systems.
1.1 What's Really in an IMU?
At its core, an IMU contains two fundamental sensors:
- Accelerometers: Measure specific force (all reaction forces except gravity)
- Gyroscopes: Measure angular rate (how fast the body is rotating in 3D space)
Think of it this way: if you're in a elevator accelerating upward, the accelerometer measures both the elevator's acceleration AND the gravity you're pushing against.
1.2 The Critical Insight: Specific Force ≠ Acceleration
This is one of the most fundamental concepts:
What accelerometers measure = (All real forces except gravity) / mass
Or mathematically:
This matter tremendously because when our rover is perfectly stationary on Earth, the accelerometers don't read zero, they measure approximately 9.8 m/s² upward (the normal force counteracting gravity). This is why we must "remove gravity" during inertial navigation.
1.3 Why Simulate Errors? The Realism vs. Theory Gap
Real IMUs are far from perfect. Simulation lets us bridge this gap:
- Test algorithms under controlled, repeatable error conditions
- Understand how different error types accumulate over time
- Develop robust filters before investing in expensive hardware
- Isolate specific error effects that would be impossible to separate in real data
🎯 LEVEL 2: Calculating the "Ground Truth" IMU Data
Before we can add realistic errors, we need perfect measurements derived from the rover's known trajectory. This reverse-engineering is surprisingly complex.
2.1 Rotation Matrices: The Language of 3D Orientation
While Euler angles (roll, pitch, yaw) are intuitive for humans, rotation matrices are computationally superior for navigation. Euler angles can suffer from "gimbal lock" (losing a degree of freedom when two axes align), while rotation matrices avoid this entirely.
A rotation matrix describes how to transform vectors from one coordinate frame to another. For the aerospace sequence (yaw → pitch → roll), we build the rotation from three elemental rotations:
The rotation from body to navigation frame (NED) is constructed as:
Where the elemental rotation matrices are:
When expanded, the complete rotation matrix becomes:
where
c=cos, s=sin.
Geometric Interpretation:
- Column 1: Direction of body X-axis in NED coordinates
- Column 2: Direction of body Y-axis in NED coordinates
- Column 3: Direction of body Z-axis in NED coordinates
Key Properties:
- The matrix is orthonormal:
- Determinant is +1 (preserves orientation)
- Composition of rotations is simple matrix multiplication
Pseudo-code Implementation
FUNCTION create_rotation_matrix(roll_angle, pitch_angle, yaw_angle)
// Precompute trigonometric functions
cos_roll = COS(roll_angle)
sin_roll = SIN(roll_angle)
cos_pitch = COS(pitch_angle)
sin_pitch = SIN(pitch_angle)
cos_yaw = COS(yaw_angle)
sin_yaw = SIN(yaw_angle)
// Build the combined rotation matrix (roll × pitch × yaw)
rotation_matrix = [
// Row 1
[cos_pitch * cos_yaw,
cos_pitch * sin_yaw,
-sin_pitch],
// Row 2
[sin_roll * sin_pitch * cos_yaw - cos_roll * sin_yaw,
sin_roll * sin_pitch * sin_yaw + cos_roll * cos_yaw,
sin_roll * cos_pitch],
// Row 3
[cos_roll * sin_pitch * cos_yaw + sin_roll * sin_yaw,
cos_roll * sin_pitch * sin_yaw - sin_roll * cos_yaw,
cos_roll * cos_pitch]
]
RETURN rotation_matrix
END FUNCTION
// Usage example:
body_to_ned_rotation = create_rotation_matrix(roll_rad, pitch_rad, yaw_rad)
// Transform a vector from body to NED frame:
vector_ned = MATRIX_MULTIPLY(body_to_ned_rotation, vector_body)
2.2 The Complete Rotation Model: It's Not Just About the Vehicle
The orientation change between time steps comes from two sources:
- Body rotation: The rover's own movement (turning, tilting)
- Earth rotation: The Earth spinning under us at ~15 degrees per hour
// Both rotations must be combined
body_rotation = calculate_from_angular_velocity(gyro_measurements)
earth_rotation = calculate_earth_rotation_at_current_latitude()
// The total rotation is the combination of both
total_rotation = combine_rotations(body_rotation, earth_rotation)
2.3 Rodrigues' Rotation Formula: Efficient 3D Updates
For discrete time steps, we need an efficient way to update rotation matrices. Rodrigues' rotation formula gives us this:
Where:
// Given an angular velocity vector [wx, wy, wz] and time step dt
rotation_vector = angular_velocity × time_step
rotation_matrix = rodrigues_formula(rotation_vector)
// This efficiently computes the rotation matrix
// It's really useful for real-time applications
2.4 The Specific Force Transformation
Now we can compute what ideal accelerometers would measure:
// Step 1: Get the true acceleration from position changes
true_acceleration_ecef = calculate_acceleration_from_trajectory()
// Step 2: Get gravity at current position
gravity_vector_ecef = calculate_gravity_model(current_latitude, current_height)
// Step 3: Compute specific force in ECEF frame
specific_force_ecef = true_acceleration_ecef - gravity_vector_ecef
// Step 4: Transform to body frame (what the physical sensors would measure)
specific_force_body = transpose(rotation_matrix) × specific_force_ecef
🎯 LEVEL 3: Simulating Realistic IMU Errors
Now we transform our ideal measurements into the noisy, imperfect readings that real sensors produce. This is where simulation meets reality.
3.1 The Zoo of IMU Errors
Real IMUs suffer from multiple error sources that interact in complex ways:
-
Biases: Constant offsets that slowly drift over time
- Example: Always reading 0.1 m/s² even when stationary
-
Scale Factors: Gain errors in the measurement chain
- Example: 1% error means 1 m/s² becomes 1.01 m/s²
-
Cross-Coupling: Axis misalignment and interference
- Example: X-axis acceleration affecting Y-axis measurements
-
Noise: Random high-frequency variations
- Example: Small random fluctuations around the true value
-
Quantization: Digital resolution limits
- Example: Can only measure in 0.01 m/s² increments
3.2 Mathematical Error Model
Each error type has a specific mathematical representation:
// The complete error model for one axis
measured_value = (
(true_value + constant_bias + time_varying_bias)
× (1 + scale_factor_error)
+ cross_coupling_from_other_axes
+ random_noise
+ quantization_error
)
3.3 Implementing Realistic Error Simulation
Here's how we apply errors in practice:
FUNCTION simulate_imu_errors(ideal_measurements, error_parameters)
// Initialize output measurements
realistic_measurements = copy(ideal_measurements)
FOR each time_step IN realistic_measurements:
// Apply biases (constant + slow drift)
realistic_measurements.gyro_x[time_step] = (
ideal_measurements.gyro_x[time_step]
+ error_parameters.gyro_bias_x
+ calculate_bias_drift(time_step)
)
// Apply scale factors
realistic_measurements.gyro_x[time_step] = (
realistic_measurements.gyro_x[time_step]
× (1 + error_parameters.gyro_scale_x)
)
// Apply cross-coupling between axes
realistic_measurements.gyro_x[time_step] = (
realistic_measurements.gyro_x[time_step]
+ error_parameters.cross_coupling_xy × ideal_measurements.gyro_y[time_step]
+ error_parameters.cross_coupling_xz × ideal_measurements.gyro_z[time_step]
)
// Add random noise
realistic_measurements.gyro_x[time_step] = (
realistic_measurements.gyro_x[time_step]
+ generate_gaussian_noise(error_parameters.noise_std)
)
// Apply quantization based on sensor resolution
realistic_measurements.gyro_x[time_step] = (
round(realistic_measurements.gyro_x[time_step] / resolution) × resolution
)
END FOR
RETURN realistic_measurements
3.4 The Complete Measurement Pipeline
Putting it all together:
// Input: Perfect trajectory data
// Output: Realistic IMU measurements
ideal_angular_rates = calculate_ideal_gyro_measurements(true_trajectory)
ideal_specific_forces = calculate_ideal_accel_measurements(true_trajectory)
realistic_angular_rates = simulate_imu_errors(ideal_angular_rates, gyro_error_params)
realistic_specific_forces = simulate_imu_errors(ideal_specific_forces, accel_error_params)
The Complete Simulation Pipeline
Putting all three levels together gives us a powerful IMU data generator:
// Input: Reference trajectory (ground truth)
// Output: Realistic IMU measurements
true_trajectory = load_reference_data()
// Level 2: Work backward from trajectory to ideal measurements
ideal_imu = calculate_ideal_imu_measurements(true_trajectory)
// Level 3: Add realistic errors
realistic_imu = simulate_imu_errors(ideal_imu, imu_error_parameters)
// Output: Ready-to-use IMU data for testing fusion algorithms
RETURN realistic_imu
Validation and Critical Insights
The validation process revealed several key insights:
- Visual Analysis: By plotting the simulated IMU data against the ground truth, I could immediately spot if the error models were behaving realistically. The noise should be random, biases should be constant, and scale factors should proportionally affect all measurements.
- Error Correlation: Different error types create distinctive patterns in the data. White noise appears as high-frequency jitter, while bias drift creates low-frequency wander in the measurements.
- The Gravity Test: A quick sanity check—when the rover is stationary and level, the accelerometers in the body frame should measure the gravity vector. If the rover is perfectly aligned with the NED frame, this would be [0, 0, 9.8] m/s², but in practice, the measured specific force depends on the rover's orientation relative to the local vertical.
Conclusion: Foundation for Sensor Fusion
Building this IMU simulator has been an incredible journey through the physics of inertial navigation. From understanding the fundamental concept of specific force to implementing sophisticated error models, each level built upon the previous one.
The most valuable lesson? Simulation forces you to understand the system at a deeper level. You can't simulate what you don't understand.
This IMU simulator now provides the perfect foundation for the next exciting phase: IMU Mechanization. In the next article, I'll show how we can take these simulated measurements and reconstruct the rover's trajectory through numerical integration and discover why IMUs alone can never provide long-term accurate navigation.
But that's a story for next time...
For those working with inertial systems: What aspects of IMU error modeling have you found most challenging? Are there particular error sources that surprised you with their impact on navigation performance?
If you've made it this long, thank you and hope this is useful content for your endeavors. See you in the next one!
Top comments (0)