Aha, that’s what I’m talking about! 250w Odrive XY lightplacer.
Is it possible to place latching hall sensors on the ML5010 type stator? Are U using the encoder on the dagor to commutate the motor? Maybe someone else has experience with mounting hall sensors to a stator. Will it work to simply glue the sensors on top of the stator, close to the edge?
I see these motors has a 850w max continues rating, so driving them @ 250-350w should keep them cool, right? I assume they have enough torque to drive a XY pnp robot in FOC mode?
I’m just using the diametric magnet and the AS5147 sensor on the Dagor board. Why mess around with hall sensors anymore? Your time and money on that will be more expensive than the $6 AS5147 and $0.25 magnet.
Ok I see, I would have a encoder on the reduction gear and use the hall sensors to commutate the motor. Another way would be to use a sensored bldc, but this would be even more expensive.
No, I would commutate with an encoder these days. Then, if you want another encoder after the reduction gears, just use a 2nd encoder. I think SimpleFOC does a good job keeping track of the motor position without needing an encoder on the other side of the reduction gears. However, after power off, it is a challenge to know the real position of device after the reduction gears. On my robot arm, I’m just going to rely on the user to always place the arm back into a rough home position to achieve the 0 position after power off.
I have to say, I’m with @jlauer on this one… I mean, unless you want to undertake the hall-sensor self-made encoder project for its own sake…
But I think you will find it quite a challenge. The encoder ICs have many hall elements, and sophisticated filtering to achieve the accuracy they get from a very close diametrical magnet. Directly sensing the rotor magnets from the side will be challenging, and I think you’ll need quite a bit of filtering (perhaps a DSP?) afterwards to get a useful signal. But maybe I don’t fully understand the concept…
So if the goal is motor commutation, I’d definately go for a pre-made sensor… if the goal is learning about magnetic sensing, then your way is much better
You both have good point´s. My main goal is to drive high current LED´s without limiting resistors I know, i know… its for another forum. The motor part is more or less a bonus and I am in it to hopefully develop my PnP machine. To drive steppers like 2 phase BLDC´s would be a great achievement and I believe there could possibly be a way to do that with the homebrewed encoder. There is alot of focus on steppers around openPnP, but as you see in the link I posted earlier, definitely eboard outrunners can do a very nice (fast) job. They do come at a higher price point, but at the end of the day, sometimes you have to pay for the torque at higher speed. But who says it has to be the cheapest possible solution. There are infact folks building high end Pnp´s around openPnp. Good thing the eboard is on the rise, and prices on those motors are going down. I will try some sensored BLDC´s and use a encoder for the precision work. If the DIY encoder fails, I will most likely fall back on the IC encoder in a breakout format.
Is the motion planner ready for SimpleFoc library? (accel/ decel/ trapezoidal movement)
were you able to move this motor? I have a similar motor but it makes a lot of noise.
Yeb, but the FETs blew up at one point which made me realize, that these motors consume to much power.
Many thanks for sharing the video. Yes, I also realise these motors consume a lot of power and they make similar type of noise.
I have read the INO sketches, very interesting concept.
Are you operating SimpleFOC in torque mode? or angle control mode?
Thanks
Once you solve this problem, you find you have more to solve. For example
-
The maximum acceleration depends on the 3D position of the arm. For example holding a 1 kG mass 1 meter from the base has higher moment of inertia than holding the same mass closer to the base. It would be a waste to always use the worst case of maximum payload at maximum distance, so you have to continuously re-compute the limits
-
path planning. How does the arm move from (x1, y1, z1) to (x2, y2, z2)? For VERY small distances a straight line works, but with 6 motors you want all 6 to complete their work at the same time and each needs to run at a different rate. One of them will be the “slowest” and limit the speed of the others. Worse, which one is slowest will change over the course of the move. So you need to check at each end of the 3D line.
-
over longer distance a straight line might not work because of collision with itself or with surroundings. It gets worse if there is a payload in the “hand” as now you have to find if the payload will collide with the arm or surroundings.
I bring all this up because, if you think it is simple, you might be temped to write it yourself. But if you know what is coming, you might look to use an exiting robotic motion planner that takes care of all of the above and even the case of using multiple arms in the same space while avoiding collisions.
You will find that a good motion planner requires simulation because it needs to look ahead in time to plan a path through 3D space.
The good news is that this is a mostly-solved problem
look here: https://moveit.ros.org/
Hi @Geronimo
Just in case you are still interested on having a Trapezoidal Planner or for for anyone else interested interested on this topic, using code created by @jlauer some time ago I have created a library to implement a trapezoidal trajectory planner that works on top of SimpleFOC.
Basically is his code encapsulated in a C++ class instead oaf a bunch of methods and global variables.
TrapezoidalPlanner.h :
#ifndef __TRAPEZOIDAL_PLANNER__H
#define __TRAPEZOIDAL_PLANNER__H
#include <SimpleFOC.h>
class TrapezoidalPlanner
{
public:
TrapezoidalPlanner(int);
void doTrapezoidalPlannerCommand(char *command);
void linkMotor(BLDCMotor*);
void runPlannerOnTick();
bool isPlannerMoving();
private:
BLDCMotor* motor;
unsigned long plannerTimeStap;
int plannerPeriod; // 1000 / this number = Hz, i.e. 1000 / 100 = 10Hz, 1000 / 10 = 100Hz, 1000 / 5 = 200Hz, 1000 / 1 = 1000hZ
float Vmax_; // # Velocity max (rads/s)
float Amax_; // # Acceleration max (rads/s/s)
float Dmax_; // # Decelerations max (rads/s/s)
float Y_;
float Yd_;
float Ydd_;
float Tf_, Xi_, Xf_, Vi_, Ar_, Dr_, Vr_, Ta_, Td_, Tv_, yAccel_;
unsigned long plannerStartingMovementTimeStamp;
bool isTrajectoryExecuting;
float sign(float val);
float sign_hard(float val);
bool calculateTrapezoidalPathParameters(float Xf, float Xi, float Vi, float Vmax, float Amax, float Dmax);
void startExecutionOfPlannerTo(float newPos);
void computeStepValuesForCurrentTime(float currentTrajectoryTime);
};
#endif
TrapezoidalPlanner.cpp
#include <Arduino.h>
#include <TrapezoidalPlanner.h>
//#define __debug
TrapezoidalPlanner::TrapezoidalPlanner(int tickPeriod ){
plannerPeriod = tickPeriod;
isTrajectoryExecuting = false;
return;
}
void TrapezoidalPlanner::linkMotor(BLDCMotor *motorReference){
motor = motorReference;
}
bool TrapezoidalPlanner::isPlannerMoving(){
return isTrajectoryExecuting;
}
void TrapezoidalPlanner::doTrapezoidalPlannerCommand(char *gCodeCommand){
#ifdef __debug
Serial.print("GGode command: G");
Serial.println(gCodeCommand);
#endif
// Parse this string for vals
String commandSrt = String(gCodeCommand);
float commandValue;
switch (gCodeCommand[0]){
case 'V':
// Remove V so can convert to a float
commandSrt = commandSrt.substring(1);
commandValue = commandSrt.toFloat();
Vmax_ = commandValue;
// Try calling the planner to use this new velocity value
// We have to use the current pos, vel, and accel
// calc_plan_trapezoidal_path_at_start(Xf_, Xi_, Vi_, Vmax_, Amax_, Dmax_);
calculateTrapezoidalPathParameters(Xf_, motor->shaft_angle, motor->shaft_velocity, Vmax_, Amax_, Dmax_);
#ifdef __debug
Serial.print("User wants velocity change. Vmax_: ");
Serial.println(Vmax_);
#endif
break;
case 'A':
// Remove A so can convert to a float
commandSrt = commandSrt.substring(1);
commandValue = commandSrt.toFloat();
Amax_ = commandValue;
Dmax_ = Amax_;
// Try calling the planner to use this new acceleration value
// calc_plan_trapezoidal_path_at_start(Xf_, Xi_, Vi_, Vmax_, Amax_, Dmax_);
calculateTrapezoidalPathParameters(Xf_, motor->shaft_angle, motor->shaft_velocity, Vmax_, Amax_, Dmax_);
#ifdef __debug
Serial.print("User wants acceleration change. Amax_: ");
Serial.println(Amax_);
#endif
break;
case 'L':
// TODO TODO TODO TODO
// TODO TODO TODO TODO
break;
default:
commandValue = commandSrt.toFloat();
#ifdef __debug
Serial.print("Move to new position (rads): ");
Serial.println(commandValue);
#endif
// We start moving to the new position
startExecutionOfPlannerTo(commandValue);
break;
}
}
void TrapezoidalPlanner::runPlannerOnTick(){
// This should get entered 100 times each second (100Hz)
if ((unsigned long)(millis() - plannerTimeStap) > plannerPeriod){
plannerTimeStap = millis();
// see if we are in a move or not
if (isTrajectoryExecuting){
// we are in a move, let's calc the next position
float timeSinceStartingTrajectoryInSeconds = (millis() - plannerStartingMovementTimeStamp) / 1000.0f;
computeStepValuesForCurrentTime(timeSinceStartingTrajectoryInSeconds);
motor->target = Y_;
// see if we are done with our move
if (timeSinceStartingTrajectoryInSeconds >= Tf_){
// we are done with move
// motor.monitor_downsample = 0; // disable monitor
#ifdef __debug
Serial.println("Done with move");
#endif
isTrajectoryExecuting = false;
}
}
}
}
float TrapezoidalPlanner::sign(float val){
if (val < 0)
return -1.0f;
if (val == 0)
return 0.0f;
// if val > 0:
return 1.0f;
}
float TrapezoidalPlanner::sign_hard(float val){
if (val < 0)
return -1.0f;
return 1.0f;
}
bool TrapezoidalPlanner::calculateTrapezoidalPathParameters(float Xf, float Xi, float Vi, float Vmax, float Amax, float Dmax){
float dX = Xf - Xi; // Distance to travel
float stop_dist = (Vi * Vi) / (2.0f * Dmax); // Minimum stopping distance
float dXstop = sign(Vi) * stop_dist; // Minimum stopping displacement
float s = sign_hard(dX - dXstop); // Sign of coast velocity (if any)
Ar_ = s * Amax; // Maximum Acceleration (signed)
Dr_ = -s * Dmax; // Maximum Deceleration (signed)
Vr_ = s * Vmax; // Maximum Velocity (signed)
// If we start with a speed faster than cruising, then we need to decel instead of accel
// aka "double deceleration move" in the paper
if ((s * Vi) > (s * Vr_)){
Ar_ = -s * Amax;
}
// Time to accel/decel to/from Vr (cruise speed)
Ta_ = (Vr_ - Vi) / Ar_;
Td_ = -Vr_ / Dr_;
// Integral of velocity ramps over the full accel and decel times to get
// minimum displacement required to reach cuising speed
float dXmin = 0.5f * Ta_ * (Vr_ + Vi) + 0.5f * Td_ * Vr_;
// Are we displacing enough to reach cruising speed?
if (s * dX < s * dXmin){
// Short move (triangle profile)
Vr_ = s * sqrt((Dr_ * sq(Vi) + 2 * Ar_ * Dr_ * dX) / (Dr_ - Ar_));
Ta_ = max(0.0f, (Vr_ - Vi) / Ar_);
Td_ = max(0.0f, -Vr_ / Dr_);
Tv_ = 0.0f;
}
else{
// Long move (trapezoidal profile)
Tv_ = (dX - dXmin) / Vr_;
}
// Fill in the rest of the values used at evaluation-time
Tf_ = Ta_ + Tv_ + Td_;
Xi_ = Xi;
Xf_ = Xf;
Vi_ = Vi;
yAccel_ = Xi + Vi * Ta_ + 0.5f * Ar_ * sq(Ta_); // pos at end of accel phase
#ifdef __debug
Serial.println("--------------------------------- ");
Serial.println(" Calculated trapezoidal Values: ");
Serial.println(" Tf: " + String(Tf_));
Serial.println(" Ta: " + String(Ta_));
Serial.println(" Tv: " + String(Tv_));
Serial.println(" Td: " + String(Td_));
Serial.println(" --------------------- ");
Serial.println(" Ar: " + String(Ar_));
Serial.println(" Vr: " + String(Vr_));
Serial.println(" Dr: " + String(Dr_));
Serial.println(" --------------------- ");
Serial.println(" Xf: " + String(Xf));
Serial.println(" Xi: " + String(Xi));
#endif
return true;
}
void TrapezoidalPlanner::startExecutionOfPlannerTo(float newPos){
// set our global of the new position
Xf_ = newPos;
// At this poin we are atarting to move following the trapezoidal profile
plannerStartingMovementTimeStamp = millis();
// take the position from SimpleFOC and set it to our start position
Xi_ = motor->shaft_angle;
// TODO: If we are being asked to move but are already in motion, we should go with the current velocity, position, and acceleration
// and keep moving.
// Now we need to do our calcs before we start moving
calculateTrapezoidalPathParameters(Xf_, Xi_, Vi_, Vmax_, Amax_, Dmax_);
motor->target = Y_; // could possibly put this in the method above
// Tell user how long this move will take
#ifdef __debug
Serial.println("Starting to move to a new position");
Serial.print("Time to complete move (secs):");
Serial.println(Tf_);
// Velocity and Accel of move
Serial.print("Velocity for move: ");
Serial.print(Vmax_);
Serial.print(", Acceleration: ");
Serial.println(Amax_);
#endif
// set our global bool so the tick method knows we have a move in motion
isTrajectoryExecuting = true;
}
void TrapezoidalPlanner::computeStepValuesForCurrentTime(float currentTrajectoryTime){
// Step_t trajStep;
if (currentTrajectoryTime < 0.0f){
// Initial Condition
Y_ = Xi_;
Yd_ = Vi_;
Ydd_ = 0.0f;
//Serial.println("--- Initial Stage ---");
#ifdef __debug
Serial.print(String(currentTrajectoryTime) + ",");
Serial.print(Y_, 5);
Serial.println();
#endif
}
else if (currentTrajectoryTime < Ta_){
// Accelerating
Y_ = Xi_ + Vi_ * currentTrajectoryTime + 0.5f * Ar_ * sq(currentTrajectoryTime);
Yd_ = Vi_ + Ar_ * currentTrajectoryTime;
Ydd_ = Ar_;
#ifdef __debug
// Serial.print("Accelerating ");
Serial.print(String(currentTrajectoryTime) + ",");
Serial.print(Y_, 5);
Serial.println();
#endif
}
else if (currentTrajectoryTime < Ta_ + Tv_){
// Coasting
Y_ = yAccel_ + Vr_ * (currentTrajectoryTime - Ta_);
Yd_ = Vr_;
#ifdef __debug
// Serial.print("Coastting ");
Serial.print(String(currentTrajectoryTime) + ",");
Serial.print(Y_, 5);
Serial.println();
#endif
}
else if (currentTrajectoryTime < Tf_){
// Deceleration
float td = currentTrajectoryTime - Tf_;
Y_ = Xf_ + 0.5f * Dr_ * sq(td);
Yd_ = Dr_ * td;
Ydd_ = Dr_;
// Serial.print("Decelerating ");
#ifdef __debug
// Serial.print("Accelerating ");
Serial.print(String(currentTrajectoryTime) + ",");
Serial.print(Y_, 5);
Serial.println();
#endif
}
else if (currentTrajectoryTime >= Tf_){ // Final Condition
Y_ = Xf_;
Yd_ = 0.0f;
Ydd_ = 0.0f;
#ifdef __debug
//Serial.print("--- Final Stage ---");
Serial.print(String(currentTrajectoryTime) + ",");
Serial.print(Y_, 5);
Serial.println();
#endif
}
else{
// TODO: Error halnling here
}
return;
}
To use it you just need to declare an instance of this class and register it using the Commander. Yuo also need to provide a reference to your motor and at the loop() call at least every 10 ms the function runPlannerOnTick().
....
TrapezoidalPlanner planner(10);
void doPlanner(char *cmd){
planner.doTrapezoidalPlannerCommand(cmd);
}
void setup(){
......
// GCode move Gxx, GVxx, or GAxx - Example: G30 moves to position in rads.
// GV10 sets velocity to 10 rads/s. GA5 sets acceleration to 5 rads/s/s.");
planner.linkMotor(&motor);
commander.add('G', doPlanner, "Motion Planner");
......
}
void loop() {
......
commander.run();
planner.runPlannerOnTick();
......
}
Here you can see an example of a capture of a trajectory using SimpleFOCStudio, in this case it is more triangular than trapezoidal.
Here you can see another capture moviing from 0 to 20 rads with a maximum velocity of 10 rads/s and acceleration of 10 rads/s^2
This looks spectacular. Nice work. I just re-read the code and after not seeing it for awhile, I’m quite happy with how it all worked so well. What use case are you using for the trapezoidal movements?
Hi @jlauer
The idea is to use it in a robotic arm if some day I finish the my eternally unfinished project. Basically is your code encapsulated in a class
Hey @JorgeMaker and @jlauer,
This has been on my bucket list for some time now
Awesome work integrating the solution!
I think we could make a simplefoc official library out of this solution.
And the simplefoclibrary for the next release will include a support for feed forward velocity which will enable even better following. In that case you’ll be able to use both position Y_
and the velocity Yd_
as targets for the motion control.
I’m looking forward to testing this code, nice work!
A quick test of the code
- max vel: 100rad/s
- max accel: 500 rad/s^2
without velocity feed-forward:
with the feedforward:
Looking at what O’Drive offers in terms of control modes there are other modes of control that can be interesting to integrate in SimpleFOC like: