Skip to content

Final project

My final project idea.

My family goes to the beach a lot. And one problem we always have is bringing all of our stuff to the beach. We have a beach cart, but it’s flimsy and hard to handle. I want to make a motorized beach cart that will automatically follow somebody with a remote. Here’s a drawing of what my idea looks like

VL531LX sensors

For my input, I know that a distance sensing system is going to be very important for my project. The distance sensor that I chose is VL53L1X ToF ranging sensor. I chose this sensor because it comes in a small footprint, and has percise, long-range distance sensing. The VL53L1X sensors in our lab come on the VL531X-SATEL breakout board. This board uses an I2C connection to communicate with the sensor.

Output- Motor

For my final project, I’m going to need some kind of DC motor to move the wagon forwards. While I don’t have the motor I want to use for the wagon yet, I found this large 12v motor in our lab. I decided that this was a good starting point, because I’ll have to use an external power supply in my final project, and I knew that I would have to for this 12v motor.

Wiring

Because I’m using a DC motor, I needed to use a motor controller so that I can control the speed and direction of the motor. The motor driver I decided to use is the L298A motor driver. So, before I started wiring, I took a look at the datasheet and pinout for the driver. A couple of things that stood out to me were that there’s about a 2 volt drop between the input voltage and the output voltage to the motors, and that the maximum voltage for the driver is 57V, which is much larger than I plan on ever using. When I looked at the pinout, I felt that I understood it pretty well, so I began wiring. For the microcontroller, I decided to use the Fab Xiao, because I didn’t know if I wanted to mill a board for this ouput or a different one yet. One mistake that I caught early on was that at first, I didn’t connect the 2 control pins to analog pins on the microcontroller. These pins need to be analog so that the motor can be controlled at different speeds. My final wiring was Analog pin 0 and 1 to IN1 and IN2, 12 volts from a power supply into Vss, 5v from the microcontroller into Vs, and the 2 wires from the motor into OUT1 and OUT2. I found a library and used an example code, making sure to replace the pins from the example to the correct pins. However, the motor didn’t spin. When I tested the voltage going out to the motor, it read at 4 volts, even though Ix used a 12v power supply. I made sure that I had the Vs and Vss correct, and I still was only getting around 4 volts. I changed to a different power supply, and the motor then worked fine. I’m still not sure what my initial issue was.

Daily Journal

Bellow will be a daily journal of each day, what I got done, and if I met my goals or not according to my Gantt chart.

What Got Done Today

  • Controlling motor with l298n to the point where I’m confident enough that I will be able to use larger motors
  • Started to learn using the HC-05 Module
  • Tried to use MPU 6050 as a compass
  • Milled first version of breakout board for GPS chip
  • Made Bill of Materials and figured out what parts need to be ordered.

Motors with the L298N

Before ordering and using larger motors, I wanted to make sure that I still knew how to use the L298N motor driver. I referred back to my output week, and was able to get a motor working with one of the example codes, but I wasn’t able to control the speed of the motor. I then ended up going to ChatGPT with a lot of questions about the different functions of the motor driver. What I was able to find out was how all of the jumpers work. There are 3 jumpers on the l298n. 2 of the the jumpers are the ENA and ENB pins. When the pins are connected, they are set to 5v, which automatically runs the motrors at 5 volts. However, when the jumper is removed, you can connect PWM or analog pins to the bottom pins in order to control the speed of the motors. Once I did this, I still wasn’t able to controll the speed of the motor, but I think that’s because the voltage was too low during the PWM pulses in order to controll the motor, I connected an osciloscope to the OUT pins of the motor driver, and was able to see the pwm signals in action

Using the HC-05 Module

What got done today- 6/7

  • More percise compass with setting the orientation

  • Fusion 360 file prepared for CAM

  • Full BOM created

  • GPS breakout designed and milled

Looking Back

After some time off from the project, I am back after making a few different design decisions:

  • Instead of making a matrix with the ToF sensor, I will do the following mechanics by a bluetooth controller

  • For Bluetooth, instead of the module, I’ll be using the Seeed Xiao Sense, which uses the ESP32 S3

  • I have gotten new motors with built in quadrature controllers to find the speed and position of the motors for the PID algorithm.

Making a board

This board includes the sensor module embedded onto the board, breakout I/O pins for the motor controller and VL53l1X.

Motor Control Progression

I’ve decided that instead of going straight to controlling both motors at once using the PID loop, I’ll do the following progression:

  • Control the motors using the L298n
  • Control a single motor with a PID loop
  • Control a single motor to steer with a PID loop
  • Control Both motors to steer with a PID loop

Motor control with L298n

The first type of control I did was controlling both motors using the l298N to control a heading. I used the l298N to turn one motor forwards and one motor backwards after the rotary encoders recorded a certain amount of steps. I then used the orientation sensor to continue the wagon straight after performing a 90 degree again.

I used the following code to perform this:

#include <ESP32Encoder.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>

Adafruit_BNO055 bno = Adafruit_BNO055();

// Constants for motor control
const int ENA = 9;  // Enable A pin of the L298N motor driver
const int IN1 = 8;   // IN1 pin of the L298N motor driver
const int IN2 = 7;   // IN2 pin of the L298N motor driver
const int ENB = 44;  // Enable B pin of the L298N motor driver
const int IN3 = 6;   // IN3 pin of the L298N motor driver
const int IN4 = 5;   // IN4 pin of the L298N motor driver

// Constants for encoder pins
const int LEFT_ENCODER_A = 1;   // Channel A pin of left motor encoder
const int LEFT_ENCODER_B = 2;   // Channel B pin of left motor encoder
const int RIGHT_ENCODER_A = 3;  // Channel A pin of right motor encoder
const int RIGHT_ENCODER_B = 4;  // Channel B pin of right motor encoder

// Constants for step counts
const int FORWARD_STEPS = 1000;   // Number of steps to move forward
const int TURN_STEPS = 500;       // Number of steps for a 90-degree turn

// Variables to track encoder counts
volatile long leftCount = 0;
volatile long rightCount = 0;

void setup() {
  Serial.begin(115200);

  // Initialize motor control pins
  pinMode(ENA, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(ENB, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);

  // Initialize encoder pins
  ESP32Encoder leftEncoder;
  ESP32Encoder rightEncoder;
  leftEncoder.attachFullQuad(LEFT_ENCODER_A, LEFT_ENCODER_B);
  rightEncoder.attachFullQuad(RIGHT_ENCODER_A, RIGHT_ENCODER_B);

  // Set starting count value after attaching
  leftEncoder.setCount(0);
  rightEncoder.setCount(0);

  // Start the motors
  analogWrite(ENA, 255);  // Left motor at full speed
  analogWrite(ENB, 255);  // Right motor at full speed

  // Move forward for FORWARD_STEPS
  moveForward(FORWARD_STEPS);

  // Perform a 90-degree turn using BNO055 sensor
  turn90Degrees();

  // Move forward for another FORWARD_STEPS
  moveForward(FORWARD_STEPS);
}

void loop() {
  // Empty loop, everything is done in setup
}

void moveForward(int steps) {
  // Reset encoder counts
  leftCount = 0;
  rightCount = 0;

  while (leftCount < steps || rightCount < steps) {
    // Move forward
    digitalWrite(IN1, HIGH);
    digitalWrite(IN2, LOW);
    digitalWrite(IN3, HIGH);
    digitalWrite(IN4, LOW);
  }

  // Stop the motors
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
  digitalWrite(IN3, LOW);
  digitalWrite(IN4, LOW);
}

void turn90Degrees() {
  // Read initial orientation angle
  imu::Vector<3> euler = bno.getVector(Adafruit_BNO055::VECTOR_EULER);
  float initialHeading = euler.x();

  // Perform turn
  while (abs(euler.x() - initialHeading) < 90) {
    // Turn left (adjust as needed for your robot)
    digitalWrite(IN1, LOW);
    digitalWrite(IN2, HIGH);
    digitalWrite(IN3, HIGH);
    digitalWrite(IN4, LOW);

    // Read current orientation angle
    euler = bno.getVector(Adafruit_BNO055::VECTOR_EULER);
  }

  // Stop the motors
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
  digitalWrite(IN3, LOW);
  digitalWrite(IN4, LOW);
}

// Interrupt service routines for encoder counts
void IRAM_ATTR leftEncoderISR() {
  leftCount++;
}

void IRAM_ATTR rightEncoderISR() {
  rightCount++;
}

Here is a video of the code:

Controlling a single motor with a PID loop: Understanding PID

A PID loop is a type of closed loop system, which has the goal a process reach a set point (desired value). The process variable is controlled by a control system (or a compensator), which changes the actuator output, which is the device that finds the error in the system, and then drives the change. A PID loop finds 3 types of responses: Proportianal response, Integral response, and Derivitve response. Proportional response finds the ration of the output response to the error signal. In my case, if the error of my heading has a proportional magnitude of 10, and I have my proportional gain set to 5, then the response will be 50. Integral response sums the amount of error over time, this allows accumulated error such as from sensor drift or electrical noise to be collected. The derivitive response causes the output to decrease if the variable is increasing rapidly, in my case if something caused the wagon to oscilate the derivitve response would be used to help correct it. Each of these responses can be tuned by changing the order of magnitude of change that each response will create.

Alt text

A Block Diagram of a closed control Loop

Alt text

A block diagram of a PID loop

Controlling a single motor with a PID loop: programming

With some help from chatGPT to learn how to implement a PID loop in C++, I created the following code that uses the rotarty encoders to set the motors to a certain position. In this case, the feedback and control come from the same source:

#include <L298N.h>
#include <ESP32Encoder.h>

#define CLK 9 // CLK ENCODER
#define DT 8  // DT ENCODER

ESP32Encoder encoder;

const unsigned int IN1 = 44;
const unsigned int IN2 = 7;
const unsigned int EN = 3;

L298N motor(EN, IN1, IN2);

const float Kp = 1.0; // Proportional gain
const float Ki = 0.0; // Integral gain
const float Kd = 0.0; // Derivative gain

const float setpoint = 1000.0; // Target position
float error, integral = 0, derivative, last_error = 0;
float output;
bool firstIteration = true;

void setup()
{
  encoder.attachHalfQuad(DT, CLK);
  encoder.setCount(0);
  Serial.begin(115200);

  motor.setSpeed(70);
}

void loop()
{
  long newPosition = encoder.getCount() / 2;
  error = setpoint - newPosition;

  // Skip integral term update in the first iteration
  if (!firstIteration) {
    integral += error;
  }
  firstIteration = false;

  derivative = error - last_error;
  output = Kp * error + Ki * integral + Kd * derivative;

  // Limit the output to the motor speed range
  output = constrain(output, -255, 255);

  motor.setSpeed(abs(output));

  if (output > 0)
  {
    motor.backward();
  }
  else if (output < 0)
  {
    motor.forward();
  }
  else
  {
    motor.stop();
  }

  last_error = error;

  // Print debugging information
  Serial.print("Setpoint: ");
  Serial.print(setpoint);
  Serial.print(", Position: ");
  Serial.print(newPosition);
  Serial.print(", Error: ");
  Serial.print(error);
  Serial.print(", Output: ");
  Serial.println(output);

  delay(100); // Adjust as needed based on your control loop frequency
}

Controlling a single motor to steer using a PID loop

After getting the motor to hold a position, my next goal was to make the motor hold a heading instead. To do this I edited the original PID code to use the heading as an input instead. One problem that I encoutered at first was that when the heading was outside of 255, a value wasn’t able to be returned. That’s because the PWM values are limited from -255 to 255. To fix this, I had to make a function to scale the values to fit this.

#include <L298N.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
#include <ESP32Encoder.h>

#define CLK 9 // CLK ENCODER
#define DT 8  // DT ENCODER

ESP32Encoder encoder;
Adafruit_BNO055 bno = Adafruit_BNO055(55);

const unsigned int IN1 = 44;
const unsigned int IN2 = 7;
const unsigned int EN = 3;

L298N motor(EN, IN1, IN2);

const float Kp = 1.0; // Proportional gain
const float Ki = 0.0; // Integral gain
const float Kd = 0.0; // Derivative gain

float heading_setpoint = 0.0; // Target heading
float heading_current = 0.0;  // Current heading

float error, integral = 0, derivative, last_error = 0;
float output;

void setup()
{
  encoder.attachHalfQuad(DT, CLK);
  encoder.setCount(0);
  Serial.begin(115200);

  motor.setSpeed(70);

  // Initialize the BNO055 sensor
  if (!bno.begin())
  {
    Serial.println("Could not find a valid BNO055 sensor, check wiring!");
    while (1)
      ;
  }
  bno.setExtCrystalUse(true);
}

void loop()
{
  // Read the current heading from the BNO055 sensor
  sensors_event_t event;
  bno.getEvent(&event);
  heading_current = event.orientation.x;

  error = heading_setpoint - heading_current;

  integral += error;
  derivative = error - last_error;
  output = Kp * error + Ki * integral + Kd * derivative;

  // Limit the output to the motor speed range
  output = constrain(output, -255, 255); /// Function to scale the motor output 

  motor.setSpeed(abs(output));

  if (output > 0)
  {
    motor.forward();
  }
  else if (output < 0)
  {
    motor.backward();
  }
  else
  {
    motor.stop();
  }

  last_error = error;

  // Print debugging information
  Serial.print("Setpoint: ");
  Serial.print(heading_setpoint);
  Serial.print(", Heading: ");
  Serial.print(heading_current);
  Serial.print(", Error: ");
  Serial.print(error);
  Serial.print(", Output: ");
  Serial.println(output);

  delay(100); // Adjust as needed based on your control loop frequency
}

Last update: March 25, 2024