DIY short range Lidar, syncing threads

PROJECT:
I am building a short-range Lidar for use on an autonomous vehicle, it utilizes:
2 - Adafruit VL53L0X (Time Of Flight Sensors)
1 – Nema 17 Stepper Motor (200 Steps)
1 - Adafruit TB6612 Motor Driver
1 – Teensy 3.2 (Micro Controller)
1 – Raspberry Pi 3b (RPi)

SET UP:
I have mounted 2- Adafruit VL53L0X TOF sensors back to back on the Nema 17 Stepper motor shaft, The Stepper motor is wired to the TB6612 Motor Driver, which in turn has its control inputs wired to the teensy 3.2 GPIO pins (9, 10, 11, 12). The VL53L0X TOF sensors are also wired to the Teensy 3.2 i2c GPIO pins, the teensy 3.2 is connected to the RPi through its USB port, utilizing a USB cable only capable of data transfer this connection is utilized for sending TOF readings and nothing else. The RPi utilizing its GPIO pins controls the Teensy 3.2, the RPi can turn the Teensy 3.2 on and off, Start/Stop the Stepper from rotating, and Start/Stop the VL53L0X TOF from taking measurements.

LIDAR SETUP:
Currently the compete Lidar (Stepper + 2 VL53L0X TOF) rotates 100 steps clockwise (180 Degrees), which allows the front and back VL53L0X TOF Sensors to scan a combined 360 Degrees.

PROBLEM:
At the moment I am testing just the “Front” VL53L0X TOF Sensor which is responsible for 0 Degrees – 180 Degrees, I am testing the Lidar in a square box, I have run numerous tests, and trial code however I am unable to Map ½ of the square box, the map displays with rounded corners. I Believe it’s due to Code issues, I have not been able to sync 2 threads, 1 Thread is responsible for rotating the Stepper, the second takes the measurements from the VL53L0X TOF Sensor. Currently it takes 748 Millis to rotate the stepper 100 steps, and I have managed to get the measurement code to take 730 Millis. This is not close enough to create an accurate map.
I have tried to combine the stepper code in the same thread as the measurement code, however this causes the stepper to “stutter” and run in a “jittery” fashion.
What I need to happen is every step a measurement is taken, I was thinking a state-machine type setup with alternating a Boolean true/false before updating the ranges variable? Any suggestions as to how to either sync these threads exactly, or utilize a state-machine(ish) type setup?

#include <Stepper.h>
#include <VL53L0X.h>
#include <Wire.h>
#include <ros.h>
#include <sensor_msgs/LaserScan.h>
#include "TeensyThreads.h"
 
// change this to the number of steps on your motor
#define STEPS 100
#define HIGH_SPEED

VL53L0X frontTOF;
VL53L0X rearTOF;

ros::NodeHandle nh;

// Laser Scan
sensor_msgs::LaserScan lidar_msg;
ros::Publisher lidar_pub("/laser_scan", &lidar_msg);

float ranges[100] = {0};
int p = 0;
int id1; // thread IDS
bool begin_rotate = false; // Bool for rotate control
bool begin_measure = false; // Bool for measurement control
bool take_measure = true;
float angle_min;
float angle_max;
const float Pi = 3.14159; // PI Constant 
float deg = 0; // deg LIDAR is currently at
 
Stepper stepper(STEPS, 9, 10, 11, 12);
 
void setup(){
  Wire.begin();
  
  pinMode(0, OUTPUT);
  digitalWrite(0, LOW);
  pinMode(23, OUTPUT);
  digitalWrite(23, LOW);

  #if defined LONG_RANGE
  // lower the return signal rate limit (default is 0.25 MCPS)
  frontTOF.setSignalRateLimit(0.1);
  // increase laser pulse periods (defaults are 14 and 10 PCLKs)
  frontTOF.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
  frontTOF.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);
  #endif

  #if defined HIGH_SPEED
  // reduce timing budget to 20 ms (default is about 33 ms)
  frontTOF.setMeasurementTimingBudget(20000);
  #elif defined HIGH_ACCURACY
  // increase timing budget to 200 ms
  frontTOF.setMeasurementTimingBudget(200000);
  #endif

  pinMode(0, INPUT);
  frontTOF.init(true);
  frontTOF.setAddress((uint8_t)22);
  frontTOF.setTimeout(500);

  
  pinMode(23, INPUT);
  rearTOF.init(true);
  rearTOF.setAddress((uint8_t)25);
  rearTOF.setTimeout(500);


  pinMode(3,INPUT); // Initialize LIDAR rotation control pin
  attachInterrupt(digitalPinToInterrupt(3), control_rotate, RISING);
  pinMode(4,INPUT); // Initialize LIDAR measure control pin
  attachInterrupt(digitalPinToInterrupt(4), control_measure, RISING);

  // Initialize ROS node handle, advertise and subscribe the topics
  nh.initNode();
  nh.getHardware()->setBaud(112500);
  nh.advertise(lidar_pub);

  // set the speed of the motor to 30 RPMs
  stepper.setSpeed(130);

  // Initialize LIDAR rotation thread and suspend 
  std::thread th_rotate(rotate_stepper);
  th_rotate.detach();
  id1 = th_rotate.get_id();
  threads.suspend(id1);
}

void rotate_stepper(){
  int i = 0;
  do {
    // Rotate stepper 180 degrees clockwinse
    while(i <= 100){
      stepper.step(1);
      // Determine rotation angle in Radians(Rads)
      angle_min = deg*Pi/180;
      angle_max = ((deg+1.8)*Pi/180);
      deg = deg + 1.8;
      ++i;
    }

    // Rotate stepper 180 degrees counterclockwise
    while(i >= 0){
      stepper.step(-1);
      // Determine rotation angle in Radians(Rads)
      angle_max = deg*Pi/180;
      angle_min = ((deg-1.8)*Pi/180);
      // Ensure roatation angle never reads below 0.0
      if(deg >= 0) { deg = deg - 1.8;}
      else { deg = 0; }
      --i;
    }
  } while(1);
}
 
void loop(){
  while(begin_measure == true) {
    p = 0;

    lidar_msg.header.frame_id = "laser_frame";
    lidar_msg.angle_min = 0.0;
    lidar_msg.angle_max = 3.14159;
    lidar_msg.angle_increment = 0.0314159;
    lidar_msg.range_min = 0.05;               // minimum range value [m]
    lidar_msg.range_max = 1.2;                // maximum range value [m]
    
    unsigned long scan_start = millis();
    
    while(p <= 99) {
      if(take_measure == true) {
      ranges[p] = (float)frontTOF.readRangeSingleMillimeters()/1000.0;
      ++p;
      }
    }
    lidar_msg.scan_time = (millis()-scan_start)/1000.0;
    lidar_msg.ranges_length = p;
    lidar_msg.ranges = ranges;
    lidar_msg.header.stamp = nh.now();
    lidar_pub.publish(&lidar_msg);
    nh.spinOnce();
  }
}

void control_rotate(){
    // Begin or cease LIDAR rotation depending on pin state
    if(begin_rotate == false) {
      threads.restart(id1);
      begin_rotate = true;

    }
    else {
      threads.suspend(id1);
      begin_rotate = false;

    }
}

void control_measure(){
    if(begin_measure == false) {
      begin_measure = true;
    }
    else  {
      begin_measure = false;
    }
}

VISUAL:
This should resemble 1/2 a box, not have curved front and back.

Nema-17.pdf (760 KB)

Shouldn't you be syncing the sensor with the motor steps? Send the pulse to the motor at the same time as you pulse the sensor. Get rid of the stepper library and pulse the motor yourself. Be careful you don't send the pulses too fast.

Forget the threads and try this pseudocode instead.

Loop:
send_step_command;
wait_until_position_stable;
get_and_store_range;
if (not_done) loop;

These links may help
Stepper Motor Basics
Simple Stepper Code

...R

@jremington, I have tried that version of code before deciding on threads, I used this:

#include <Stepper.h>
#include "Adafruit_VL53L0X.h"
#include <ros.h>
#include <sensor_msgs/LaserScan.h>
 
// change this to the number of steps on your motor
#define STEPS 100

Adafruit_VL53L0X frontTOF = Adafruit_VL53L0X();
Adafruit_VL53L0X rearTOF = Adafruit_VL53L0X();

ros::NodeHandle nh;

// Laser Scan
sensor_msgs::LaserScan lidar_msg;
ros::Publisher lidar_pub("/laser_scan", &lidar_msg);

float ranges[100] = {0};
int p = 0;
bool begin_rotate = false; // Bool for rotate control
 
// create an instance of the stepper class, specifying
// the number of steps of the motor and the pins it's
// attached to
Stepper stepper(STEPS, 9, 10, 11, 12);
 
void setup(){
  pinMode(0, OUTPUT);
  digitalWrite(0, LOW);
  pinMode(23, OUTPUT);
  digitalWrite(23, LOW);

  digitalWrite(0, HIGH);
  frontTOF.begin(0x3F);
  digitalWrite(23, HIGH);
  rearTOF.begin(0x30);

  pinMode(3,INPUT); // Initialize LIDAR rotation control pin
  attachInterrupt(digitalPinToInterrupt(3), control_rotate, RISING);
  
  // Initialize ROS node handle, advertise and subscribe the topics
  nh.initNode();
  nh.getHardware()->setBaud(115200);
  nh.advertise(lidar_pub);

  // set the speed of the motor to 30 RPMs
  stepper.setSpeed(400);
}
 
void loop(){
  while(begin_rotate == true) {
    VL53L0X_RangingMeasurementData_t measure;
    
  
    lidar_msg.header.frame_id = "laser_frame";
    lidar_msg.angle_min = 0.0;
    lidar_msg.angle_max = 3.14159;
    lidar_msg.angle_increment = 0.0314159;
    lidar_msg.range_min = 0.05;               // minimum range value [m]
    lidar_msg.range_max = 1.2;                // maximum range value [m]
    unsigned long scan_start = millis();
    while(p < 99) {
      frontTOF.rangingTest(&measure, false); // pass in 'true' to get debug data printout!
      if (measure.RangeStatus != 4) {  // phase failures have incorrect data
        ranges[p] = (float)measure.RangeMilliMeter/1000.0;
      }
      stepper.step(+1);
      ++p;
    }
    lidar_msg.scan_time = (millis()-scan_start)/1000.0;
    lidar_msg.ranges_length = 100;
    lidar_msg.ranges = ranges;
    lidar_msg.header.stamp = nh.now();
    lidar_pub.publish(&lidar_msg);
    nh.spinOnce();

    // Rear

    lidar_msg.header.frame_id = "laser_frame";
    lidar_msg.angle_min = 3.14159;
    lidar_msg.angle_max = 6.28318;
    lidar_msg.angle_increment = 0.0314159;
    lidar_msg.range_min = 0.05;               // minimum range value [m]
    lidar_msg.range_max = 1.2;                // maximum range value [m]
    
    scan_start = millis();

    while(p >= 0) {
      rearTOF.rangingTest(&measure, false); // pass in 'true' to get debug data printout!
      if (measure.RangeStatus != 4) {  // phase failures have incorrect data
        ranges[p] = (float)measure.RangeMilliMeter/1000.0;
      }
      stepper.step(-1);
      --p;
    }

    lidar_msg.scan_time = (millis()-scan_start)/1000.0;
    lidar_msg.ranges_length = 100;
    lidar_msg.ranges = ranges;
    lidar_msg.header.stamp = nh.now();
    lidar_pub.publish(&lidar_msg);
    nh.spinOnce();
  }
}

void control_rotate(){
    // Begin or cease LIDAR rotation depending on pin state
    if(begin_rotate == false) {
      begin_rotate = true;
    }
    else {
      begin_rotate = false;
    }
}

it results in very "jittery"/"Jerky" stepping movements.

@DKWatson & Robin2, I will look at ditching the stepper library and pulsing the motor my self while polling the TOF Sensor.

EDIT:
I also just noticed this version used the Adafruit VL53L0X library which was giving me troubles of its own, ill try using the standard VL53L0X library and run it again.

it results in very "jittery"/"Jerky" stepping movements

As others have pointed out, you are using the wrong library, and probably, the wrong motor driver.

You don't need any library to control a stepper. If using a proper stepper driver, just set the motor driver DIR input to the desired state and pulse the STEP input at the appropriate intervals. Takes all of 3-4 lines of code.

Note that the Adafruit TB6612 driver will not work properly with most modern, low impedance steppers, as it lacks appropriate current limiting. The A4988, DRV8825 or MP6500 drivers from Pololu are much better choices.

Please post a link to the exact stepper motor product page or data sheet, and state the voltage and current capabilities of the motor power supply.

Greetings.

I am using a tfMini on top of a rotating servo, which sits upon a gyro stabilized platform.

I use <Servo.h> and writeMicroseconds to rotate the LIDAR servo.

With a 180 degree servo, a single degree of microseconds is 5.554. OK, it is a bit more but I found 5.554 per degree works very well for 1 degree.

I increment/decrement, depending on direction, the LIDAR servo, 3.0 microseconds per aspect change and batch fire the LIDAR using single triggers.

I do not use a LIDAR servo delay time to allow the servo to settle. The increments are very small. I, also, figure the code gives the servo enough time to go from one aspect to the next; works well.

I monitor the signal strength to get the lowest value before deciding that is the correct distance; a group of 3 low signal strength values with the LIDAR fired at 40Hz (25 milliseconds), I discard the 1st 3 return pulses as they, typically, are high signal strength low distance readings.

I blank, not fire, the LIDAR during aspect changes, which is why I use the LIDAR single trigger mode. Short aspect changes lower the time of LIDAR blanking and gives better resolution.

I do rotate the LIDAR slowly (Python, if((iRunTimeSeconds % 3) ==0), which works for my application.

I have used LIDAR servo rotations down to 1.5 microseconds per aspect change of the LIDAR servo.

I use a RPi to determine LIDAR servo aspect change times and aspect change increments/decrements. Even though it is overkill, I use a 20Kg metal geared servo to rotate the LIDAR module, which also contains the MPU6050 that maintains X/Y level.

I found that for long term reliability and to minimize hysteresis a metal geared servo works much better than the plastic geared servo.

Anyways, the image of the return pulses you posted look like the LIDAR has not settled down to the new distance of the aspect change.

I noticed that the tfMini's first few pulses at a new position return short distances and a strong signal strength and as the pulse sequence fires the signal strength drops and the distance increases.

Try a monitor of the signal strength of the LIDAR during the scan of the curved section, you might find the signal strength/distance changing during a corner scan and may want to work out a signal strength to distance ratio that you'll find acceptable.

1). aspect change = one movement of the LIDAR servo.

@jremington, Motor Data Sheet has been attached to the original post, Power supply is 5v, 2A.

@Idahowalker, I will look at monitoring signal strength from the scans, I need to dig into VL53L0X.cpp to see if that is possible with this device.

As always, thanks for the help everyone!

Motor Data Sheet has been attached to the original post, Power supply is 5v, 2A.

Indeed, the Adafruit TB6612 driver is totally unsuitable for that motor, and the motor power supply is inadequate.

I recommend to use one of the Pololu A4988, DRV8825 or MP6500 stepper drivers instead, and a 12-24V, 2A (or greater) power supply. Be careful to set the current limit to 1.0 A/winding.

@jremington, I do agree 99% of the time a chopper driver is your best choice, I did choose to use an H-Bridge in this project, I have broken down my calculations below, I wouldn't mind a second set of eyes.

With a stalled motor, using the TB6612, 5V, and a motor resistance of 3.5 Ω the possible current at transient time or a stall state is.

5V/3.5A = 1.42A p/phase

With a rotating stepper, less current is used due to inductance, looking at 2-time constants only.

Time Constant:
T = L/R
T = (0.0045H/3.5 Ω)
T = 0.0013 S (1.3ms)

1t:
I(t) T=0.0012:
I(t) = Vs/R(1-e^-Rt/L)
I(t) = 5/3.5(1-e^3.5*0.0013/0.0045)
I(t) = 1.428(1-.393)
I(t) = 910mA p/phase

2t:
I(t) t=0.0024:
I(t) = 5/3.5(1-e^3.5*0.0024/0.0045)
I(t) = 1.428(1-.154)
I(t) = 1.2A

Measured current of the rotating stepper is 870mA p/phase consistent with the current over time calculations, peak current while rotating is 900mA p/phase, and stalled current p/phase is 940mA, total current draw on a stall as measured from power supply is 1.75A.

Response:

jremington:
Indeed, the Adafruit TB6612 driver is totally unsuitable for that motor, and the motor power supply is inadequate.

I don’t wholly agree that the TB6612 is unsuitable to drive this stepper, I do agree that a Pololu “Chopper” Driver is a more efficient solution.

I do concede that a scenario exists where 1.4A Amps on a single phase could ruin my day, but I have accounted for this.

TB6612 Max Current: 3A @ 20mS

Stepper Operating Current: ~ 900mA – 1.1A (Manufacture tolerance)

Even at a full 2A from the power supply the TB6612 would be ok unless a pin was left high, the digitalWrite() command writes a pin high for 5 μs with 16 Mhz clock, only way TB6612 would be damaged is if the pin was held high longer than 20mS and it was pulling above 1.2A.

As far as motor over current, max this motor is going to get Is 1.4A p/phase, looking at anywhere from 600mA to 500mA above specifications, realistically due to inductance this would be in a stall state or at transient time, and the RPi monitors for stall states, detects, then resets the Teensy controlling the driver, therefor cutting current to the motor.

I did make a choice to not use a chopper driver, main factor is eventually this project will be moved from perf board and break out boards to PCB. At that time, I am ditching the breakout boards, and it is going to be easier to include a dual H-Bridge, rather than recreate a chopper driver.

As far as motor over current, max this motor is going to get Is 1.4A p/phase

Times 2 phases = 2.8A from your 2A power supply?

Good luck with the project.