A proposal for a new robot balancing robot

This is the first draft of a proposal to create a 2-wheeled self-balancing robot library to show off some of the capabilities of the new Arduino UNO R4 WIFI.

My intention is to receive comments and critiques from people who are willing to contribute to this fun project. I currently have a prototype robot made with a 3D printer, and an iPhone that is running TouchOSC for controlling it. That enables me to be one of the beta testers of this library. There is more to add to this proposal, and I have started a skeleton library and example code for testing.

(As an aside, my robot is using stepper motors that are more than 60 years old and came from a printer that weighed more than me. Surprisingly, they work fine)

If this is something you are interested in supporting, please let me know how you can contribute. Could probably use a document reviewer, coders, beta testers. While I can do a lot of the coding, I am probably not the right guy for the more complex timer stuff, conversions and calculations, and accel/decell stuff.

I can provide the .stl files for anyone who wants to print one of these toys. I would just need a few details like the steppers you are using, the battery's dimensions, etc.

Here is the draft, please keep the comments, suggestions, and criticisms friendly.

Proposed Library for UNO R4 WIFI to Balance 2-wheel Robots Using Stepper Motors.
REV 0.1 11/16/2023

Purpose
The new Arduino UNO R4 WIFI board needs a library that shows off some of its capabilities in a fun way. One such opportunity is remotely controlling a 2-wheel self-balancing robot.

The reason a library for these is necessary is beginner programmers and makers don’t have the skills necessary to develop these fun toys. One obstacle to making a self-balancing robot is finding a way to control the 2 steppers independently. It is more difficult than it seems at first glance. The robot also requires two timers, WIFI / Bluetooth and communications functions for remote control, stepper drivers, and an IMU for balance. They occasionally fall down, so servos can move arms that lift the robot back up on its own. These are difficult challenges.

Many of these obstacles can be simplified by a well written library. Here are some of the ways to accomplish that.

Stepper motors offer a good choice of motors because they can be precisely controlled and there is no backlash that can cause excess wobble.

The Arduino UNO R4 WIFI is a good starting point for the controller because it is fast 48MHz, has lots of memory, has built-in WIFI / Bluetooth and accepts shields that were designed for the UNO R3.

This library can be used for many other purposes, the initial focus however is on 2-wheel self-balancing robots because they require most all the functions of any balancing system.

To begin, let’s look at the required hardware.

Hardware Required
• Arduino UNO R4 WIFI
• IMU, MPU6050, BNO055 or similar (I2C connected)
• 2 stepper motors (any modern stepper)
• Stepper driver shield
• Optionally, 2 servos to raise a fallen robot
• Battery 12vdc
• Power switch
• 3D Printed robot frame and wheels
• Assorted M3 screws and nuts

The Library Description
The library provides functions to instantiate a motor driver, configure the driver pins, set the input mode, enable or disable the stepper motors, set the desired RPM and direction, and set the Acceleration steps-per-second rate. Default values are set so a beginner using only the basic setup will see some motion in the motors when he powers the robot on. There is internal support for up to 2 servos that can be used to raise a fallen robot, and a report feature that provides status data via the Serial Monitor. It also has TouchOSC functions to send and receive data to and from a remote controller wirelessly. The remote controller can be a PC, Android, or an IOS device running TouchOSC.

The library is written in a straightforward manner avoiding confusing and complex code with lots of comments to help a beginner understand what is happening. It is coded using the accepted standard Arduino programming rules and styles.

Library functions
The publically available functions are listed here:

• StepperClass();
used to instantiate the motor driver class

• void initialize(int _stepPin, int _dirPin);
configures the step and direction pins for this motor driver

• void enableSteppers(int _enablePin);
enables BOTH motors

• void disableSteppers(int _enablePin);
disables BOTH motors

• void setRPM(int _desiredRPM);
sets the RPM and direction of the stepper motor
Note: although not absolutely necessary, entering the desired RPM it allow for serial output to display human useful information. It also would be a useful feature if the library is used for something other than a self-balancing robot.

• void setAccelStepsSec(boolean _accelStepsSec);
sets the acceleration steps per second for the stepper motor

• void setInputMode(boolean _mode);
determines whether the input RPM input is DIRECT_RPM or PERCENT_MAX_RPM. In DIRECT_RPM mode the input is the desired RPM, in PERCENT_MAX_RPM, the input is a percentage of the MAX_RPM.

• void setMaxRPM(int _maxRPM);
Sets the maximum allowed stepper RPM

• void raiseRobot(); is used to raise the robot if it is tilted beyond a controllable angle.

• reportStatus(); is a function in the example code that sends status data to the Serial monitor periodically. The data includes current the RPM for both motors, the current velocity of the robot, the battery voltage, the PID values currently in use.

• char OSCReadMSG();reads the messages from the connected remote control

• void OSCSendMSG(); sends messages to the remote control

Other libraries
The MPU6050_tockn library manages the Inertial Management Unit (IMU
The PID (PID_v1) library for the PID functions.

1 Like

Consider using a modern IMU. Both the MPU-6050 and the BNO055 were discontinued and have been obsolete for many years. Any of the modern replacements will significantly outperform either one.

Delta_G,

The size is determined primarily by the stepper motors and the battery used. Mine is quite large because the 50 rear old steppers are at least Nema 23 size if not bigger. Here are a few pics.



JRemington, Thanks for the advice, I'll check out some new IMU types.

Well, I did more than I thought I could on this, and my robot is nearly standing on it' own. I am using the FspTimer timers and though it's a little awkward to program them, they are working. I'm having a little trouble with one of the 50 year old steppers. If I can't clear it, I may need to switch to something more recent which means making a new body and wheels. I'll do that eventually anyway because these old ones and the huge battery weigh a ton. It will be tough for the servos to raise it when it falls over.

This is here for review. Positive comments, criticisms welcome.

I'll be out of town this weekend. When I get back on Monday, I'll probable make a new robot using standard NEMA 14 motors and work on this code some more.

example.ino

#include "robot_stepper.h"
#include <PID_v1.h>
#include "FspTimer.h"

//#define PID_DEBUG
#define IMU_DEBUG

FspTimer M1_timer;
FspTimer M2_timer;

/************************************************************************************************************************/
#include <MPU6050_tockn.h>  // will change to SparkFun 9DoF IMU Breakout - ICM-20948 (Qwiic) when parts arrive
/************************************************************************************************************************/

#include <Wire.h>

/************************************************************************************************************************/
MPU6050 robotIMU(Wire);  // will change to SparkFun 9DoF IMU Breakout - ICM-20948 (Qwiic) when parts arrive
/************************************************************************************************************************/

#define ENABLE_PIN   4                                  // a common enable pin for both of my motors
#define STEP1_PIN    7                                  // pulses LOW for 1 usec to move motor 1 one step
#define DIR1_PIN     8                                  // determines direction, FWD/REV for motor 1
#define STEP2_PIN   12                                  // pulses LOW for 1 usec to move motor 2 one step
#define DIR2_PIN     5                                  // determines direction, FWD/REV for motor 2
#define MAX_SPEED  100                                  // sets the maximum stepper speed in RPM

// PID parameters
double setpoint, input, output;                         // variables we'll be connecting to
double cons_kp = 1, cons_ki = 0.05, cons_kd = 0.25;     // conservative tuning Parameters (when the robot nearly upright)
double agg_kp = 4, agg_ki = 0.2, agg_kd = 1;            // aggressive tuning Parameters (when the robot is far from upright);
double acc, gyro, accOld, gyroOld;                      // current and old data from the imu
unsigned long loopTimer, loopTimerOld;                  // main loop timer
unsigned long reportTimer, startReportTimer;

#define ACC_STEPS_SEC 10                                // the user's acceleration rate

StepperClass M1;                                        // the motor 1 instance
StepperClass M2;                                        // the motor 2 instance

//Specify the links and initial tuning parameters
PID robotPID(&input, &output, &setpoint, cons_kp, cons_ki, cons_kd, DIRECT);

// setup sets the initial state of the motor driver pins and each motor's unique timer
/************************************************************************************************************************/
void setup() {
  Serial.begin(115200);
  delay(500);
  pinMode(ENABLE_PIN, OUTPUT);                          // this is the motor driver enable pin
  digitalWrite(ENABLE_PIN, HIGH);                       // a HIGH disables the motors while we configure things
  M1.initialize(STEP1_PIN, DIR1_PIN);                   // initialize motor1 and its timer
  M2.initialize(STEP2_PIN, DIR2_PIN);                   // initialize motor2 and its timer
  M1.getFrequency(0);                                        // motors run in opposite directions for straight robot movement
  M2.getFrequency(0);                                        // motors run in opposite directions for straight robot movement
  M1.setAccelStepsSec(10);                              // sets the number of steps per sec increase or decrease for acceleration
  digitalWrite(ENABLE_PIN, LOW);                        // now we can enable the motors (a LOW enables the motors)
  setpoint = 0;                                         // <set to whatever value the imu reports when it is uoright>
  robotPID.SetMode(AUTOMATIC);
  robotPID.SetOutputLimits(-100, 100);                     // These limits will be useful for the timer calculations

  // start the IMU
  Wire.begin();
  /************************************************************************************************************************/
  robotIMU.begin();
  // robotIMU.calcGyroOffsets(true);
  /************************************************************************************************************************/

  //turn the PID on
  robotPID.SetMode(AUTOMATIC);
  loopTimerOld = millis();

  beginTimerM1(M1.getFrequency(25));
  beginTimerM2(M2.getFrequency(25));

  delay(2000);
}

// loop
/************************************************************************************************************************/
void loop() {
  //  loopTimer = millis();
  //  if (loopTimer - loopTimerOld > 20) {
  loopTimerOld = loopTimer;

  /************************************************************************************************************************/
  robotIMU.update();// will change to SparkFun 9DoF IMU Breakout - ICM-20948 (Qwiic) when parts arrive
  /************************************************************************************************************************/

  double gap = abs(setpoint - input); //distance away from setpoint

  if (gap < 10)
  { //we're close to setpoint, use conservative tuning parameters
    robotPID.SetTunings(cons_kp, cons_ki, cons_kd);
  }
  else
  {
    //we're far from setpoint, use aggressive tuning parameters
    robotPID.SetTunings(agg_kp, agg_ki, agg_kd);
  }

  // position the imu sensor on the robot so the X angle changes when the robot is tilted back and forth
  /************************************************************************************************************************/
  input = robotIMU.getAngleX();   // will change to SparkFun 9DoF IMU Breakout - ICM-20948 (Qwiic) when parts arrive
#ifdef IMU_DEBUG
  Serial.print("angle "); Serial.println(input);
#endif
  /************************************************************************************************************************/

  /* <do any necessary imu signal filtering here>
      Examples:
      acc  = acc * 0.8 + accOld * 0.2;
      accOld = acc;
      gyro = gyro * 0.8 + gyroOld * 0.2;
      gyroOld = gyro;
  */

  // run the PID routine

  robotPID.Compute();                                        // output value will be -100 to 100

#ifdef PID_DEBUG
  Serial.print("PID Input "); Serial.print(input);
  Serial.print("  PID output "); Serial.println(output);
#endif

  float m1, m2;
  m1 = M1.getFrequency(output);
  m2 = M1.getFrequency(output);

  Serial.print(m1); Serial.print(" "); Serial.println(m2);

  M1_timer.set_frequency(m1);
  M2_timer.set_frequency(m2);

  reportTimer = millis();
  if (reportTimer - startReportTimer > 2000);             // send a report to the Serial monitor every 2 second
  reportStatus();
}
//}

// reportStatus
/************************************************************************************************************************/
void reportStatus() {
}

// callback method used by M1_timer
void M1_callback(timer_callback_args_t __attribute((unused)) *p_args) {
  //  digitalWrite(M1.stepPin, LOW);
  delayMicroseconds(2);
  digitalWrite(M1.stepPin, HIGH);
}

// callback method used by M2_timer
void M2_callback(timer_callback_args_t __attribute((unused)) *p_args) {
  digitalWrite(M2.stepPin, LOW);
  delayMicroseconds(2);
  digitalWrite(M2.stepPin, HIGH);
}

// beginTimerM1
/************************************************************************************************************************/
bool beginTimerM1(float rate) {
  uint8_t timer_type = GPT_TIMER;
  int8_t tindex = FspTimer::get_available_timer(timer_type);
  if (tindex < 0) {
    tindex = FspTimer::get_available_timer(timer_type, true);
  }
  if (tindex < 0) {
    return false;
  }

  FspTimer::force_use_of_pwm_reserved_timer();

  if (!M1_timer.begin(TIMER_MODE_PERIODIC, timer_type, tindex, rate, 0.0f, M1_callback)) {
    return false;
  }

  if (!M1_timer.setup_overflow_irq()) {
    return false;
  }

  if (!M1_timer.open()) {
    return false;
  }

  if (!M1_timer.start()) {
    return false;
  }
  return true;
}

// beginTimerM2
/************************************************************************************************************************/
bool beginTimerM2(float rate) {
  uint8_t timer_type = GPT_TIMER;
  int8_t tindex = FspTimer::get_available_timer(timer_type);
  if (tindex < 0) {
    tindex = FspTimer::get_available_timer(timer_type, true);
  }
  if (tindex < 0) {
    return false;
  }

  FspTimer::force_use_of_pwm_reserved_timer();

  if (!M2_timer.begin(TIMER_MODE_PERIODIC, timer_type, tindex, rate, 0.0f, M2_callback)) {
    return false;
  }

  if (!M2_timer.setup_overflow_irq()) {
    return false;
  }

  if (!M2_timer.open()) {
    return false;
  }

  if (!M2_timer.start()) {
    return false;
  }
  return true;
}

robot_stepper.cpp

#include "robot_stepper.h"

StepperClass::StepperClass() {};

/* initialize gets the motor ready by initializing its unique timer and
   configuring the pins for the stepper motor.
  initialize */
/************************************************************************************************************************/
void StepperClass::initialize(int _stepPin, int _dirPin) {
  stepPin       = _stepPin;
  dirPin        = _dirPin;
  pinMode(stepPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  digitalWrite(dirPin, HIGH);
}

/************************************************************************************************************************/
float StepperClass::getFrequency(float _desiredRPM) {
  desiredRPM = constrain(_desiredRPM, -maxRPM, maxRPM);
  desiredRPM = map(_desiredRPM, -maxRPM, maxRPM, maxRPM, -maxRPM);

  if (_desiredRPM >= 0)
    digitalWrite(dirPin, HIGH);
  else
    digitalWrite(dirPin, LOW);

  float rps = _desiredRPM / 60;
  float frequency = 3200 * rps;
 
  return abs(frequency);
}

// setAccelStepsSec
/************************************************************************************************************************/
void StepperClass::setAccelStepsSec(boolean _accelStepsSec) { // the default is DIRECT, but PERCENTAGE is also supported
  accelStepsSec  = _accelStepsSec;
}

// setMaxRPM
/************************************************************************************************************************/
void StepperClass::setMaxRPM(int _maxRPM) {
  maxRPM = _maxRPM;
}

// setStepsPerRevolution
/************************************************************************************************************************/
void StepperClass::setStepsPerRevolution(int _steps) {
  stepsPerRevolution = _steps;
}

// raiseRobot
/************************************************************************************************************************/
void StepperClass::raiseRobot() {
  /* If the robot is tilted past controllable angle, operate
      the servs(s) to raise it to a controllable state.
  */
}

// disable
/************************************************************************************************************************/
void StepperClass::disableSteppers(int enablePin) {
  // stop the motor
  getFrequency(0);

  // the disable the motors
  digitalWrite(enablePin, HIGH);
}

// enable
/************************************************************************************************************************/
void StepperClass::enableSteppers(int enablePin) {
  digitalWrite(enablePin, LOW);
}

// OSCReadMsg
/************************************************************************************************************************/
char StepperClass::OSCReadMsg() {
  // <tbd>
}

// OSCSendMsg
/************************************************************************************************************************/
void StepperClass::OSCSendMsg() {
  // <tbd>
}

robot_stepper.h

#ifndef __ROBOT__
#define __ROBOT__

#include <Arduino.h>

// StepperClass
/************************************************************************************************************************/
class StepperClass {

  public:
    enum
    {
      DIRECT_RPM,
      PERCENT_MAX_RPM,
      MOVE_FWD,
      MOVE_REV
    };

  private:
    int       dirPin;
    int       stepsPerRevolution = 200;
    int       maxRPM;
    int       desiredRPM = 20;
    int       currentRPM = 0;
    int       accelStepsSec = 5;
    boolean   currentDirection = MOVE_FWD;
    boolean   mode = DIRECT_RPM;
    unsigned  long startTime, currentTime;

  public:
    int       stepPin;

    StepperClass();
    void initialize(int _stepPin, int _dirPin);
    void enableSteppers(int _enablePin);
    void disableSteppers(int _enablePin);
    float getFrequency(float _desiredRPM);
    void setStepsPerRevolution(int _steps);
    void setAccelStepsSec(boolean _accelStepsSec);
    void setInputMode(boolean _mode);
    void setMaxRPM(int _maxRPM);
    void raiseRobot();
    char OSCReadMsg();
    void OSCSendMsg();
};

#endif

Yes I do. I’ll put it the on Monday when I get home.

NEMA 17 will work great. I’ll make a design for 17s and 14s.

Sounds like a good idea to me. Also, if you have an android the TouchOSC works with that and it works with a PC too.

Would be cool if it could work with a PS4 controller too but that would definitely be a future item.

Anyway, gotta get it to stand up first. Gotta crawl before you can run.

Something I thought of last night is servo control. All it needs is a timer and very little code. That would obviate the need for using another library.

I’m going to add the Sparkfun 9DOF IMU. It has the QWIIC connector that will plug right into the R4.

As I understand it, there are 2 AGT timers. One is used for both millis() and micros(). I agree with using AGT for the servos, but not the one serving those two functions. I want to preserve millis and micros if we can.

In another post you made it clear that you would rather use your own timer rather than FSB.
Are you working on one or do you already have one?

At a minimum, the robot will require throttle and steering inputs from the remote control. Later we could add kp, ki, kd adjustments and others. You mentioned exposing API's, but I'm not sure how those work.

I put the current code in github at github/PickyBiker. It's private so just let me know what your github name is and I'll give you access. I have never worked on a project with someone before, so I need to learn how to actually use github for that purpose.

The .stl files for a NEMA 17 size robot are posted in github. The tire was printed with TPU and the rest was PETG although any material would probably be okay.

I'm working accel/decel without using the acceleration library. This will finish the motor driver code. and make us ready for communication, OSC and API.

Looks like baby is crawling now. Really nice to see someone make what I built do what it was designed for. This is real progress.

I put all your code together in one folder for testing. One thing I don't see is where you are making the enable pin LOW to enable the steppers. Is that in the code somewhere that I don't see, or have you tied it low?

LOL, I looked for quite a while trying to find that.
I'll just enable that in main .ino for now.

I do understand turning a trace into a fuse. Been there many times.

The CNC boards do have a fuse, but you are right, I should put a fuse on the new PCB design.

Interesting, it should have blown, and I don't see the fuse on the schematic. If I remember correctly, the VIN came from Pin 8 of the Z or A stepper via a short piece of hookup wire. Unless you moved that directly to the 12v input, that fuse should have blown.

Still having trouble getting the web page stuff to work.
I am using the IP address, 192.168.1.101, and the port is 2080.
When I scan my network, I see the 192.168.1.101, and I can ping it ok.
But when I browse HTTP://192.168.1.101 it returns:
image
I turned off the firewall, and still get the same results.

It is on the same network. The code has 2080 and 192.168.1.101 and it does the same thing with or without the :2080

Not sure why it isn’t working. Headed out on date night with the wife. I’ll look into it mor in the AM

Here is the new PCB Shield. Got to check it over carefully tomorrow and then send it off.

At real low speeds, acceleration is not needed. It is needed only when there is a large change in speed. You can probably not apply acceleration to small speed changes.

Running a PID slower than scheduled only affects the time based terms. In the standard PID_v1 v 1.2.2 library it would dilute the integral component and accentuate the derivative component. It looks like you have the integral turned off in

so the only effect on late runs would be exaggerating the derivative term (which acts against changes in error (or against changes in measurement with "derivative on measurement" calculations ))

What are you updating to get a big jerk and how much is the jerk?

I like thinking of the PID constants as conversion factors between the input (error) units and the output units (e.g.: pwmcounts/degrees, pwmcounts/(degree*seconds), pwmcounts/(degrees/sec), etc...) If you can figure out the units on your constants, it makes it easier to understand what the PID is doing.

Hi guys, may I join in your project? I'm also doing exactly what you guys are doing, using the same hardware but I have been struggling a lot with the timers and finding more information into this ARM Arduino R4 WiFi. I'm trying a code that works well using everything you guys said, but ESP32-NodeMCU-S works well with that. I will take pictures of the model and base this design on this other project: GitHub - bluino/esp32_wifi_balancing_robot: An Self Balancing Robot based ESP32 can be controlled use Android over Wifi. I'm using the app which it's free. I'm 3D printing some parts, I will take pictures and surely come back with some questions.

Delta_G, I'm still having issues connecting even with the R4 WIFI example. This is not related to our project, so I will open a separate post seeking help with that.

I noticed this is your first post on the forum so, Welcome!

You are free to monitor this series of posts and contribute if you have something related to this specific project. The software being developed here is currently only works on the new R4 WIFI product and is specific to the 3D printed robot and hardware being developed. If you are seeking help with your project, you would be better served asking questions in the wider Arduino forum at Latest Using Arduino/Programming Questions topics - Arduino Forum.

Again, Welcome to the Arduino forum!