A tilt-controlled racing game for the ESP32, featuring stable motion controls via a complementary filter.

- ESP32 WROOM Development Board
- MPU6050 Gyroscope/Accelerometer Module
- 0.96-inch SSD1306 OLED Display (I2C)
- Breadboard
- Jumper Wires
The components are connected using the I2C communication protocol.
ESP32 Pin | MPU6050 Pin | OLED Display Pin |
---|---|---|
3.3V | VCC |
VCC |
GND | GND |
GND |
GPIO 22 | SCL |
SCL |
GPIO 21 | SDA |
SDA |
+-----------------+ +-----------------+ +-----------------+
| ESP32 WROOM | | MPU6050 | | OLED Display |
| | | | | |
| 3.3V ------|----->| VCC |----->| VCC |
| GND ------|----->| GND |----->| GND |
| GPIO 22 ----|----->| SCL |----->| SCL |
| GPIO 21 ----|----->| SDA |----->| SDA |
+-----------------+ +-----------------+ +-----------------+
- Install Libraries: Open the Arduino IDE Library Manager (Sketch -> Include Library -> Manage Libraries...) and install the following:
Adafruit GFX Library
Adafruit SSD1306
Adafruit MPU6050
Adafruit Unified Sensor
- Connect Hardware: Wire your components as shown in the diagram above.
- Upload Code: Open the main
esp32-gyro-racer.ino
file in the Arduino IDE, select your ESP32 board and COM port, and click Upload.
An ideal MPU6050 would be perfectly silent when still. In reality, it has two major flaws:
- Bias: When stationary, the gyroscope doesn't output a perfect zero. It has a small, non-zero offset called bias.
- Drift: If we continuously add up the gyroscope's readings to calculate the angle, this tiny bias error accumulates, causing the calculated angle to "drift" away from the true angle over time.
To combat bias, we perform a calibration routine at the start. The logic is simple: if we measure the sensor's error while it's still, we can subtract that error from all future readings.
The mathematical process is to calculate the average error over a set of samples (
In the code, we take gyroXoffset
is then subtracted from every new gyroscope reading to get a corrected rate of rotation.
Calibration alone doesn't stop all drift. To get a truly stable angle, we fuse data from two different sensors: the gyroscope and the accelerometer.
- Gyroscope: Excellent for measuring fast, short-term rotation but drifts over the long term.
- Accelerometer: Can determine the angle relative to gravity, making it stable over the long term, but it's very "noisy" and unreliable during quick movements.
A Complementary Filter combines the best of both worlds.
We can calculate the board's tilt angle using basic trigonometry. The accelerometer measures the force of gravity across its X, Y, and Z axes. For X-axis tilt (pitch), we can use the Y and Z components. Using the atan2(y, z)
function, which is more robust than atan(y/z)
, we get the angle in radians, which we convert to degrees:
Where
The complementary filter is a simple weighted average that combines the integrated gyroscope angle with the accelerometer angle. The formula in our code is:
Let's break this down:
-
$Angle_{fused, prev} + Rate_{gyro} \cdot \Delta t$ : This is the new angle predicted by the gyroscope. It takes the previous angle and adds the change measured by the gyro over the small time interval$\Delta t$ . -
$Angle_{accel}$ : This is the stable angle calculated from the accelerometer. -
$\alpha$ and$(1-\alpha)$ : These are the weighting factors. In our code,$\alpha = 0.98$ . This means we trust the responsive gyroscope for 98% of our final angle calculation, but we constantly "nudge" it with 2% of the stable accelerometer data. This nudge is enough to completely eliminate the long-term drift.
Once we have a stable fusedAngleX
, we map it to the screen coordinates. The map()
function performs a linear transformation:
playerX = map(fusedAngleX, -30, 30, SCREEN_WIDTH - playerWidth, 0);
This maps an input angle from -30° to +30° to a pixel coordinate on the screen. The output range is inverted (SCREEN_WIDTH
to 0
) to create the inverted control scheme, providing the final layer of polish to the player experience.
Contributions are welcome. If you find a bug or have a feature request, please open an issue or submit a pull request.