That only works when the motor is spinning as expected and the back EMF cancels out most of the applied voltage. When open loop stalls at high speed, the electromagnetic field continues spinning with voltage calculated on the assumption that there is back EMF, but in reality there is no back EMF so the coils see the full applied voltage and current shoots through the roof.
No, it just returns electrical_angle if there’s no sensor linked (it’s in base_classes/FOCMotor.cpp if you want to see)
Those one-line changes in velocityOpenloop and loopFOC are all there is to it. And it’s not just for exceptions. It will be continuously regulating the voltage to keep a constant current flowing in the coils regardless of speed and load.
Well, I guess its worth a shot then! I will try to get the flux observer running (which I am a bit afraid of, but I should get over that). Then I can try your proposed solution. Thanks for pointing me in the right direction. I really appreciate it.
I actually hope to have the sirens ready by the end of January even if with a sensor-based solution. So long as I can get the desired velocity, I am hopeful.
Happy new year! And greetings from the simplefoc trenches.
I want to update you with my progress (or perhaps better said lack thereof).
I tried out the flux observer. I was able to get it running (kind of) with:
BLDCMotor motor = BLDCMotor(7, 0.4, 920, 0.000045);
I say kind of because sometimes it starts rotating the wrong direction. But when it goes the right direction it seems to work ok. I have limited the voltage to 1 but no current limit.
But I really still am not understanding the play between all the variables and what phase resistance I should rely on. For example, with my 2212 930kv motor, I am consistently getting a phase reading of 2.5 ohms. Assuming the motor is wound Delta, that makes 3.75 ohms phase-to-phase resistance. No matter how I tweak the flux observer nothing works with that phase_resistance set. However, when I initialize the code and turn the motor by hand, it does seems to measure the angle and velocity regardless of the phase resistance.
On the other hand, in open loop velocity, the motor seems to run very well with:
BLDCMotor motor = BLDCMotor(7, 3.75, 920, 0.000045);
and additionally with
motor.current_limit = 0.075;
It will also run with:
BLDCMotor motor = BLDCMotor(7, 0.4, 920, 0.000045);
But I have to raise the current_limit significantly.
This is very inefficient, as for different motors with different phase resistances the same voltage values can produce wildly different currents. For gimbal motor, you can run it in the open loop with the voltage limits of 5-10 Volts and it will reach the currents of 0.5-2 amps as it has the pahse resistance from 5-15 Ohms. For drone motors, the voltage limits should stay very low, under 1 volt. Because they have phase resistance of 0.05 to 0.2 Ohms.
Any other thoughts that might lead to a successful implementation of the idea of @dekutree64 also most welcome. Its really hard to know where to start with tuning the flux observer. I have no idea if I am way off and the numbers including the PID values seem to all make a difference.
That’s right, I forgot to mention you need to set TorqueControlType::dc_current for this plan. I also didn’t notice before, but I think you’ll need to set motor.current_sp = motor.current_limit. The motor current should be constant at the setpoint, but current from the power supply should increase with RPM.
For the flux observer, I think the higher resistance is correct, but not necessarily what will work. Looking over my setup, the 0.04 in the code implies an unrealistically high torque to weight ratio for my motor. It reads 0.2 ohms by multimeter, and the listed resistance for a motor of similar size and kv here NeuMotors 3800 Series BLDC Motors: 400 to 1,600 watt class – NeuMotors Brushless Motors (search 3814/8/411 on that page) is 0.113 ohms, so that’s in the right ballpark (assuming that one is also delta terminated, and that Rm is the resistance between two wires and not a single phase). A premium motor like that should be better than my Racerstar, but I would guesstimate that my phase resistance should be around 0.2.
I can’t find much information on expected inductance for brushless motors, but the references I did find were in the 10-100uH range, so the 0.000025 I found by trial and error is reasonable.
Resistance gets multiplied by the current and dt while inductance is only multiplied by the change in current, but I think that implicitly multiplies by dt since the change will be smaller if dt is smaller. I don’t think there’s any way to get the same behavior by changing both phase_resistance and inductance, and since my inductance seems reasonable anyway, I don’t see how it could be made to work with the real resistance value.
So if my working resistance is about 20% of what it should be, try setting yours to 3.75/5=0.75 and see how it goes. My kv is also 5% higher than the listed value, so you could try 950 or so and see if it helps.
I think this is somewhat working and pretty efficiently. I can stall the motor and nothing spikes, but it also does not recover. Is that what I should expect?
On second note, this does not seem to be working as expected. Now the numbers in the Motor definition dont seem to make a difference. There does seem to be a max current on stall based on Motor.current_limit. Here is my coded in case you can see something wrong. Of course, with the changes to BLDCMotor.cpp we discussed.
// Open loop motor control example
#include <SimpleFOC.h>
#include <SimpleFOCDrivers.h>
#include <encoders/MXLEMMING_observer/MXLEMMINGObserverSensor.h>
//BLDCMotor motor = BLDCMotor(7, 0.25, 1000, 0.0000755);
BLDCMotor motor = BLDCMotor(7, 0.25, 3000, 0.0000755);
BLDCDriver6PWM driver = BLDCDriver6PWM(A_PHASE_UH, A_PHASE_UL, A_PHASE_VH, A_PHASE_VL, A_PHASE_WH, A_PHASE_WL);
LowsideCurrentSense currentSense = LowsideCurrentSense(0.003, -64.0/7.0, A_OP1_OUT, A_OP2_OUT, A_OP3_OUT);
MXLEMMINGObserverSensor sensor = MXLEMMINGObserverSensor(motor);
//target variable
float target_velocity = 1;
LowPassFilter targetFilter = LowPassFilter(2.0f);
// instantiate the commander
Commander command = Commander(Serial);
void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); }
void setup() {
// use monitoring with serial
Serial.begin(115200);
sensor.init();
//motor.linkSensor(&sensor);
// driver config
// power supply voltage [V]
driver.voltage_power_supply = 12;
driver.voltage_limit = 12;
if(!driver.init()){
Serial.println("Driver init failed!");
return;
}
// link the motor and the driver
motor.linkDriver(&driver);
motor.voltage_limit = 4; // [V]
motor.current_limit = 3;
//motor.motion_downsample = 4;
//motor.monitor_downsample = 10000;
currentSense.linkDriver(&driver);
// initialise the current sensing
if(!currentSense.init()){
Serial.println("Current sense init failed.");
return;
}
// no need for aligning
currentSense.skip_align = true;
motor.linkCurrentSense(¤tSense);
motor.sensor_direction= Direction::CW;
motor.zero_electric_angle = 0;
// open loop control config
motor.controller = MotionControlType::velocity_openloop;
motor.torque_controller = TorqueControlType::dc_current;
motor.current_sp = motor.current_limit;
// init motor hardware
if(!motor.init()){
Serial.println("Motor init failed!");
return;
}
motor.initFOC();
motor.useMonitoring(Serial);
// add target command T
command.add('T', doTarget, "target velocity");
Serial.println("Motor ready!");
Serial.println("Set target velocity [rad/s]");
_delay(1000);
}
void loop() {
motor.loopFOC();
motor.move(targetFilter(target_velocity));
//motor.monitor();
command.run();
}
Flux observer should recover from a stall, but your code as-is should not since it’s only running open loop. The next step is to combine the two. It will be something like this:
Normal state: Open loop mode, sensor not linked to motor. Call sensor.update() on flux observer and check if its angle is reasonably close to the open loop angle, motor.pole_pairs). “Reasonably close” is probably within ±pi/6.
Recovery mode: Sensor is linked to motor, so loopFOC will call update on it. Either check for some condition to see if it’s safe to switch back to open loop, or just wait some amount of time and then switch back.
For doing the angle comparison, I recommend converting to 16-bit fixed-point first. Its natural wrapping behavior is so much more elegant than doing a bunch of checks for wraparound on float angles.
Ok! This makes sense. I have started implementing it and have the linking and unlinking of the sensor coded. Just to be sure I understand correctly, in Recovery Mode, I would also need to switch: motor.controller = MotionControlType::velocity;
And let the PID controller recover to close enough. Is that correct?
Perhaps this is a bit ambitious of me… but…
If I can get this properly working, I think I have a reasonable enough understanding of the piping that I could contribute this methodology as a new controller type, maybe something like: motor.controller = MotionControlType::velocity_openloop_with_sensorrecovery;
Actually, as I think about this conceptually, I am wondering if this should be the other way around: using closed loop until it is near the target and then switching to open loop for being as accurate to the target as possible. That way, you can maintain the voltage and current settings from the PID which is probably more efficient.
Although I may have come up with an even simpler approach. Instead of a closed-loop recovery mode, just limit how far ahead of the sensor the open loop angle is allowed to get. Stick this code in BLDCMotor::velocityOpenloop and see what happens:
I will try it, I assume the earlier change in velocityOpenloop to replace setPhaseVoltage still applies : electrical_angle = _electricalAngle(shaft_angle, pole_pairs);
and to set: motor.torque_controller = TorqueControlType::dc_current;
Something is not quite right here. Below I paste my BLDCMotor::velocityOpenloop. I got an error with: int32_t openloopAngle = (int32_t)(motor.electrical_angle * (32768.0f/_PI)));
and assume you simply meant: int32_t openloopAngle = (int32_t)(electrical_angle * (32768.0f/_PI)));
It seems like angle is wrapping without the new code. Not sure if that is part of the problem. Again, theoretically I understand the concept. But something is not quite right. I guess I also do not understand how this works give that the new code uses electrical_angle, but we also have to update electrical angle for loopFOC’s call to setPhaseVoltage.
float BLDCMotor::velocityOpenloop(float target_velocity){
// get current timestamp
unsigned long now_us = _micros();
// calculate the sample time from last call
float Ts = (now_us - open_loop_timestamp) * 1e-6f;
// quick fix for strange cases (micros overflow + timestamp not defined)
if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f;
// calculate the necessary angle to achieve target velocity
shaft_angle = _normalizeAngle(shaft_angle + target_velocity*Ts);
// for display purposes
shaft_velocity = target_velocity;
if (sensor) {
int32_t sensorAngle = (int32_t)(sensor->getMechanicalAngle() * pole_pairs * (32768.0f/_PI));
int32_t openloopAngle = (int32_t)(electrical_angle * (32768.0f/_PI));
int16_t diff = (int16_t)(openloopAngle - sensorAngle);
if (shaft_velocity > 0 && diff > 32768/6) {
shaft_angle = _normalizeAngle(sensor->getAngle() + _PI_6/pole_pairs);
} else if (shaft_velocity < 0 && diff < -32768/6) {
shaft_angle = _normalizeAngle(sensor->getAngle() - _PI_6/pole_pairs);
}
}
// use voltage limit or current limit
float Uq = voltage_limit;
if(_isset(phase_resistance)){
Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit);
// recalculate the current
current.q = (Uq - fabs(voltage_bemf))/phase_resistance;
}
// set the maximal allowed voltage (voltage_limit) with the necessary angle
//setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs));
electrical_angle = _electricalAngle(shaft_angle, pole_pairs);
// save timestamp for next call
open_loop_timestamp = now_us;
return Uq;
}
Oh right, that would be using the previous frame’s electrical_angle. Better just calculate it from shaft_angle: int32_t openloopAngle = (int32_t)(shaft_angle * pole_pairs * (32768.0f/_PI)));
It will get recalculated the same way afterward by electrical_angle = _electricalAngle(shaft_angle, pole_pairs);, but we can worry about optimizing it later if it actually works.
I debated whether to use _electricalAngle to multiply by pole_pairs here as well, but I think the explicit multiply to convert mechanical degrees to electrical degrees makes it easier to understand since the constants use division by pole_pairs to convert pi/6 electrical degrees to mechanical degrees.
Still not working. Is there a way to monitor openloopAngle versus sensorAngle with downsampling? It seems that shaft_angle wraps in open loop. Whereas in closed loop, with the flux observer, it does not.
I also tried to only allow the cross over once the velocity is set to over 10 rads to see what happens when the switch kicks in. The result is that it just starts spinning at a constant speed. This suggests to me that the sensor is not updating properly. I am running loopFOC and the sensor is linked to the motor.
In closed loop, when I turn the motor, I see the shaft angle changing. And again, it does not wrap.
You are an angel. Fingers crossed for both of us regarding the power. So the behavior is strange.
I am monitoring both sensorAngle and openLoopAngle,
When I raise the velocity at which the switch can happen (15 rads/s) the following occurs:
When target vel = 0 and I turn the motor by hand, sensorAngle does not change.
When 0 < target vel < 15, both seem to loop incrementing positive but are never close.
When target vel > 15 and the switch kicks in, sensorAngle immediately goes negative and the two variable are never close.
I dont doubt that I still have not dialed in the sensor. However, the sensor is working in closed loop, but only when the pid is set properly. Of course, in open loop the motor starts in a given position with greater torque.
Adding one more observation. In closed loop with the flux sensor, the motor seems to kick a bit in the wrong direction before moving in the correct direction.
I was able to tweak the motor settings to where the sensorAngle seemed better aligned with the openloopAngle! That is good new I think. Now I can see that when I stall the motor properly the shaft_velocity remains the same as it is hard set to the target in BLDCMotor::velocityOpenloop.
Should that also need to be recalculated.
Now that I can print out the angle and velocity of the sensor compared to the openloop angle and velocity, I can still see that the sensor is still not computing the velocity very well. So probs still needs some tuning.
Final update before bed. I am realizing that there is another problem. The flux observer setting for phase_resistance needed to make the sensor work limits the the top velocity of the motor far below what it can reach in open loop with a higher, more accurate setting for phase_resistance. Is there a way around this?