Guide to Indoor Position Holding: Adding Optical Flow and LiDAR Distance Sensors to an Arduino Quadcopter
Indoor Position Holding:
Adding Optical Flow and LiDAR Distance Sensors to an Arduino Quadcopter
Flying a quadcopter indoors is a challenge because GPS signals disappear. To keep the aircraft stable you need real‑time position data. This guide shows you how to equip a standard Arduino‑based quadcopter with an optical flow sensor and a LiDAR distance sensor to achieve reliable indoor position holding.
1. What You’ll Learn
- Key hardware components and why they work together.
- Step‑by‑step wiring diagram using an
Arduino Nano 33 IoT. - Installing and configuring the
PX4Flowoptical flow library. - Integrating the
VL53L1XLiDAR sensor for altitude control. - Combining sensor data with a simple complementary filter.
- Basic PID tuning for smooth hover.
- Debugging tips and troubleshooting checklist.
2. Required Hardware
Provides built‑in IMU and enough I²C ports for both sensors.
128×128 pixel sensor + laser‑assist, communicates via I²C.
Accurate up‑to‑4 m range, I²C interface, ideal for altitude hold.
Standard 920‑1100 KV motor/ESC combo for a 250‑mm frame.
Feeds 5 V to sensors and 12 V to ESCs.
2.1 Pin Mapping (I²C)
| Component | SDA | SCL | Power |
|---|---|---|---|
| PX4Flow | A4 | A5 | 3.3 V |
| VL53L1X | A4 | A5 | 3.3 V |
| Arduino Nano 33 IoT | SDA (D11) | SCL (D12) | 5 V (regulated to 3.3 V via on‑board LDO) |
3. Preparing the Development Environment
- Install the latest Arduino IDE (v2.x recommended).
- Open Boards Manager and install
Arduino Nano 33 IoTboard package. - Open the Library Manager and add:
PX4Flow– GitHubVL53L1X– Pololu’s library.Arduino_LSM9DS1– built‑in IMU (for attitude reference).
- Enable I²C pull‑up resistors (4.7 kΩ) on SDA/SCL if your board does not have them built‑in.
3.1 Sketch Boilerplate
#include <Wire.h>
#include <PX4Flow.h> // Optical flow
#include <VL53L1X.h> // LiDAR
#include <Arduino_LSM9DS1.h> // Inertial Measurement Unit
PX4Flow flow;
VL53L1X lidar;
float roll, pitch, yaw;
// Complementary filter variables
float fusedX = 0, fusedY = 0;
const float alpha = 0.85; // weight for optical flow
void setup() {
Serial.begin(115200);
while (!Serial) {}
// Initialize I2C
Wire.begin();
// ---- Optical Flow ----
if (!flow.begin()) {
Serial.println(F("PX4Flow not detected!"));
while (1);
}
flow.setIntegrationTime(10000); // 10 ms integration
// ---- LiDAR ----
lidar.setTimeout(500);
if (!lidar.init()) {
Serial.println(F("LiDAR not detected!"));
while (1);
}
lidar.startContinuous();
// ---- IMU ----
if (!IMU.begin()) {
Serial.println(F("IMU init failed!"));
while (1);
}
Serial.println(F("All sensors initialized."));
}
void loop() {
// 1️⃣ Read optical flow (pixel displacement per sec)
if (flow.available()) {
int16_t dx = flow.getFlowX(); // positive right
int16_t dy = flow.getFlowY(); // positive forward
float dt = flow.getIntegrationTime() / 1e6; // seconds
// Convert to velocity (m/s) using sensor height (from LiDAR)
float height = lidar.readRangeContinuousMillimeters() / 1000.0;
float velX = (dx / 128.0) * height / dt;
float velY = (dy / 128.0) * height / dt;
// Complementary filter
fusedX = alpha * (fusedX + velX * dt) + (1 - alpha) * 0; // placeholder for GPS = 0
fusedY = alpha * (fusedY + velY * dt) + (1 - alpha) * 0;
}
// 2️⃣ Altitude from LiDAR
float altitude = lidar.readRangeContinuousMillimeters() / 1000.0; // m
// 3️⃣ Attitude from IMU
float ax, ay, az, gx, gy, gz;
IMU.readAcceleration(ax, ay, az);
IMU.readGyroscope(gx, gy, gz);
// Simple integration for roll/pitch (for demo only)
roll = atan2(ay, az);
pitch = atan2(-ax, sqrt(ay*ay + az*az));
// 4️⃣ Output for debugging
Serial.print(F("PosX: ")); Serial.print(fusedX,3);
Serial.print(F("\tPosY: ")); Serial.print(fusedY,3);
Serial.print(F("\tAlt: ")); Serial.print(altitude,2);
Serial.print(F("\tRoll: ")); Serial.print(roll*57.3,1);
Serial.print(F("\tPitch: ")); Serial.print(pitch*57.3,1);
Serial.println();
delay(20); // ~50 Hz loop
}
Tip: Keep the optical flow sensor at least 10 cm above a textured surface (e.g., matte paper) for reliable motion vectors. Use a small clear acrylic spacer if needed.
4. Merging Sensor Data – The Complementary Filter
The optical flow gives you relative displacement, while the LiDAR provides an absolute height reference. A complementary filter blends them, giving smooth position estimates without drift.
4.1 Filter Equation
For the X‑axis (the Y‑axis works identically):
fusedX = α * (fusedX + velX·Δt) + (1‑α) * posX_LiDAR
Because the LiDAR only measures altitude, posX_LiDAR is set to 0. The filter’s strength (α) typically ranges from 0.8 to 0.95.
4.2 Applying to the Flight Controller
Replace the placeholder demo code with your own PID loops. Below is a minimal PID sketch that uses the fused X/Y velocities to generate motor corrections.
// PID constants – start with low gains indoors
float Kp = 0.6, Ki = 0.15, Kd = 0.08;
float errX = 0, errY = 0;
float intX = 0, intY = 0;
float prevErrX = 0, prevErrY = 0;
void pidControl() {
// Desired set‑point: keep at origin (0,0) and altitude of 1 m
float setX = 0, setY = 0;
float setAlt = 1.0;
// Errors
errX = setX - fusedX;
errY = setY - fusedY;
// Integral term (simple anti‑windup)
intX += errX * 0.02;
intY += errY * 0.02;
intX = constrain(intX, -10, 10);
intY = constrain(intY, -10, 10);
// Derivative term
float dErrX = (errX - prevErrX) / 0.02;
float dErrY = (errY - prevErrY) / 0.02;
// PID output
float outX = Kp*errX + Ki*intX + Kd*dErrX;
float outY = Kp*errY + Ki*intY + Kd*dErrY;
// Altitude hold (simple P controller)
float altError = setAlt - altitude;
float outAlt = 0.4 * altError; // tune separately
// Mix outputs to motor commands (example for + configuration)
float m1 = baseThrottle + outAlt - outY + outX;
float m2 = baseThrottle + outAlt - outY - outX;
float m3 = baseThrottle + outAlt + outY - outX;
float m4 = baseThrottle + outAlt + outY + outX;
// Constrain and send to ESCs
setMotorSpeeds(constrain(m1,0,255), constrain(m2,0,255),
constrain(m3,0,255), constrain(m4,0,255));
// Store for next loop
prevErrX = errX;
prevErrY = errY;
}
5. Testing & Calibration
5.1 Ground‑Test Procedure
- Secure the quadcopter on a sturdy table.
- Open the Serial Monitor (115200 baud) and verify that optical flow values change when you gently slide the drone forward or sideways.
- Cover the LiDAR sensor with a piece of cardboard 1 m away – the altitude readout should stabilize near 1 m.
- Run the
pidControl()loop at 50 Hz (useTimerOnelibrary for precise timing). - Gradually lower the throttle while watching the altitude reading; the quad should settle at the set point before you remove power.
5.2 In‑Flight Tuning Checklist
| Parameter | Adjustment Guidance |
|---|