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 PX4Flow optical flow library.
  • Integrating the VL53L1X LiDAR 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

Arduino Nano 33 IoT

Provides built‑in IMU and enough I²C ports for both sensors.

PX4Flow Optical Flow Module

128×128 pixel sensor + laser‑assist, communicates via I²C.

VL53L1X Time‑of‑Flight LiDAR

Accurate up‑to‑4 m range, I²C interface, ideal for altitude hold.

Brushless Motors & ESCs

Standard 920‑1100 KV motor/ESC combo for a 250‑mm frame.

Power Distribution Board

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

  1. Install the latest Arduino IDE (v2.x recommended).
  2. Open Boards Manager and install Arduino Nano 33 IoT board package.
  3. Open the Library Manager and add:
    • PX4FlowGitHub
    • VL53L1X – Pololu’s library.
    • Arduino_LSM9DS1 – built‑in IMU (for attitude reference).
  4. 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

  1. Secure the quadcopter on a sturdy table.
  2. Open the Serial Monitor (115200 baud) and verify that optical flow values change when you gently slide the drone forward or sideways.
  3. Cover the LiDAR sensor with a piece of cardboard 1 m away – the altitude readout should stabilize near 1 m.
  4. Run the pidControl() loop at 50 Hz (use TimerOne library for precise timing).
  5. 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

Ready to Start?

Become Part of the ICT Club Community

Many learners are already building the technology skills that improve their daily work performance. Your journey starts today.

Parameter Adjustment Guidance