[Dprglist] Power vs Velocity for PID controllers

Murray Altheim murray18 at altheim.com
Tue Apr 21 17:56:52 PDT 2020





On 22/04/20 11:59 am, Karim Virani wrote:
> Hi Murray,
> 
> I mostly have questions. I'll be a bit verbose 'cause I might be able to
> reuse some of this with my team.

Thanks for asking. Agreed, I find these conversations enlightening even if they
don't always (apparently) lead to a quick solution.

> 1. You said you were using a Thunderborg motor controller. Is it this 
> one: https://www.piborg.org/motor-control-1135/thunderborg

Yes, that's the one.

> 2. What is your purpose - what are you trying to accomplish? Controlling an 
> arm for example is different than controlling a differential drive. Are you
> controlling for speed? For position? For differential rotation?

I'm using the ThunderBorg to control two pairs of motors, one pair for the port and
one pair for the starboard side of the robot. These are the main drive motors of
the robot. As Doug has pointed out, having two motors introduces the vagary of them
not performing identically (with encoders only on the front motors), but I'm willing
to deal with that in order to have four wheel drive.

   https://robots.org.nz/2019/12/27/after-despair-some-joy/
   https://service.robots.org.nz/wiki/Wiki.jsp?page=KR01

My intention is to use the ThunderBorgs and the motor encoders to control for
robot velocity.

> 3. I see the same problems with nomenclature all the time with my team and the
> systems we work with. With the api we use, setpower() might be a proxy for 
> setting a normalized voltage or might be a proxy for set speed depending on 
> how we've configured motor control and encoders. But looking up the thunderborg,
> I don't see evidence for any kind of feedback. There's no mention of current
> monitoring built into the motor controller. It also doesn't seem to have inputs for 
> quadrature encoders; and I didn't see any mention in your post about adding
> your own encoders in the system.

The ThunderBorg is simply a motor controller with a GetMotor() and a SetMotor()
methods in its Python support library. There is a battery voltage monitor on the
board as well, but I'm not using it. I've got a Pimoroni ADS1015 AD sensor and
with that am monitoring raw battery voltage (~20V), regulator voltage (~5V), but
that's of the entire robot system itself. I'm planning to add current monitoring
to the system in order to detect motor stall current. But there's nothing by way
of feedback from the controller itself.

I've based my robot on the OSEPP Tank, which uses two motors and a pair of OSEPP
Hall Effect sensor encoders designed specifically for the motors and their chassis
hardware. As above, I've wired a second pair of motors in parallel with the
original pair so I would have four wheel drive, with the encoders on the front
motors.

I'd originally planned to use the OSEPP silicon tank treads but the weight of my
robot (~5lbs) was too much and the treads would just tear off. I can't blame
OSEPP: their tank design is pretty lightweight, and I'm waaaay over that with
my robot. So I'm instead using four of their silicon wheels. Pretty grippy. I do
understand the limitations of having four wheels vs. tank treads vs. two wheels.
I'm (so far) willing to live with that limitation. I've got a much smaller robot
that I've mentioned in a different missive that I'm planning to implement as a
four Mecanum wheel robot, but that will entirely have its own issues.

The motor gearing is 45:1, but there's no documentation I could find on how many
pulses come from the encoders per rotation, but from my own measurements, the
encoders generate 494 pulses per wheel rotation. I ran multiple runs of 20
to 50 rotations and that's the most accurate number I can obtain.

> Maybe you over simplified the psuedo code, but going from what you posted, it 
> seems you are just doing a get_power that simply returns the power you 
> previously set in the prior run of the loop. I don't see any new feedback
> entering your algoritm. It seems to me that if your target power (normalized
> voltage) is unchanging, your error will always be zero and you'll never adjust
> the power at all. Is your error changing?

Yes, I have over-simplified the pseudo-code. The controller actually works, albeit
imperfectly. The earlier code of a few days ago had "velocity" as an argument, and
I was making a very rough conversion of velocity to intended "power" and measuring
the error between that and the "power" measurement from the controller. It is this
rough conversion I was indicating was the problem, i.e., that whether it's actually
voltage of some actual power measurement used by the ThunderBorg with its GetMotor()
and SetMotor() functions, I'm trying to figure the relationship between velocity
and power.

Note: I'll use "power" in a loose sense, as the value measured from and sent to
the motor controllers. It may be voltage, or PWM, or something else. Dunno. I
started using "power" because the ThunderBorg code was like this:

     def SetMotor2(self, power):
         """
SetMotor2(power)

Sets the drive level for motor 2, from +1 to -1.
e.g.
SetMotor2(0)     -> motor 2 is stopped
SetMotor2(0.75)  -> motor 2 moving forward at 75% power
SetMotor2(-0.5)  -> motor 2 moving reverse at 50% power
SetMotor2(1)     -> motor 2 moving forward at 100% power
         """
         if power < 0:
...

So it may be "power" in the sense of percentage of PWM.

When I simplified the controller (ignoring velocity and just having a target motor
power setting), it works okay. But of course putting some friction on the wheels
(while the robot is on the bench) just means the PID controller will continue to
push that same amount of power to the motors, not that the velocity will remain
constant.

it's the addition of velocity to the PID controller that is (obviously) at the crux
of this discussion. I think.

> 4. The concept of stacking PIDs always seems weird to me. Not that we never do
> it. There are cases where we might want a lower level motor controller to set 
> a steady velocity while a higher level controller drives toward a final position. 
>
> But these are controlling two different things. Only the low level one is 
> controlling the motor. The other is controlling the target for the low level
> controller. Typically the low level one would normally operate at a finer grain time 
> scale than the higher level controller. Otherwise it doesn't make sense to have
> two levels. Make sure you really need two controllers. It can make debugging much
> harder and might not be adding that much benefit.

I can't disagree at all, certainly you have a great deal more experience with this
than myself, and having now implemented a velocity PID over a power PID controller
this means I now have six variables to tweak (VP, VI, VD, PP, PI and PD). It occurred
to me to somehow incorporate the velocity into the existing power PID controller
but that's where I'm running a bit short on the experience/knowledge/math.

But you've very correctly deduced my rationale for the two levels. The higher level
velocity PID controller would set velocity targets and itself control the power
PID controller. Whether that complexity is warranted is to me entirely unknown; I
just don't know how else to do it.

> We have to think clearly about what we are trying to do. For a simple DC motor
> we typically only have control over one output and that is voltage. The voltage
> will generally relate to speed given a ton of real world caveats. Note I didn't 
> say the voltage is perfectly linearly proportional to speed. It's because of 
> those caveats that we take input from encoders to correct the system. But we 
> need to decide what we are trying to control and that will give us how we calculate 
> your error. Yup, velocity is just the derivative of position and automatically
> calculates in your PID algoritm if it's based on encoder input. But if you want
> to control velocity then your error should be the difference between your target
> and measured velocities.

Agreed. But I can't send a SetVelocity() to the motors, only SetMotor() with I
think "power" between 0.0 and 1.0. In looking at the TB code I'm guessing they
mean "power" as PWM ratio, but it's not documented AFAIK.

> 5. Going back to your psuedo code - I don't see any correction for the time
> between loops. I see this in a lot of PID implementations - they often assume
> a constant time between calls. Because we use cooperative multitasking we never
> assume that we have a constant elapsed time and we always normalize inputs to  the amount of elapsed time between feedback measurements. Be aware this also intrisically converts measurements to velocity, but the benefit is that if your control 
> loop changes duration or varies in duration, it won't require you to retune your PID constants.

My pseudocode didn't include it, but yes, my actual code is measuring loop time.

> 6. Also in your psuedo code - I don't see any handling for integral windup.
> That's a whole other level of discussion, so I won't go into it now. I would
> just be careful about introducing the integral - we find it to be the most tricky of 
> the terms to tune and control. This could also bring up a whole discussion of
> the regimes over which each of the P, I and D terms operate - which could also
> be an extensive discussion. The question is, do you need the I term at all? It's 
> the term than brings in the near-zero precision to your control system, but 
> can also wreak a lot of havoc if you don't have windup control. David talks 
> about a leaky integrator as one of his favorite anti windup mechanisms. There 
> are a variety of others and the optimal combination might be application specific.

I'm very much still catching up to where David was at years ago.

I'm currently using a slewing mechanism to ramp up volume gradually so the motors
don't take a big hit, and I've commented the I term out on occasion, but no, I
don't understand nor have I implemented anything like integral windup.

Over the weekend I spent many hours trying to port Brett Beauregard's Arduino PID
code, also based on his article at:

   http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/

It was appealing because a) it is proven to function on the Arduino, and b) it has
a lot of features. Unfortunately that complexity meant I never managed to get it
running, after hours and hours of trying. Having a Python port of it would have
been nice...

> 7. I just saw your update. So you do have encoders in the system. But you might
> not be using them yet? Again, is your error actually ever non zero? You have to
> calculate your error from the difference between what your sensors/encoders are 
> actually measuring and what you want them to be (your setpoint). It makes sense
> to us to first scale our encoder input to some kind of natural units. For example
> we will calibrate the number of ticks_per_meter or ticks_per_degree (or 
> radian) that maps our encoder readings to the real world. Drive your robot for one second, measure how far it moved, divide into the number of ticks it moved during that run. Then we'll have a low level functions that scale those encoder 
> values to our real world units and change them to velocities if needed.

Yes, using the encoders. I've gotten to the point where (last week) I had that
imperfect velocity-to-power PID working, and could drag on the wheels of the robot
and it would compensate by powering up. It worked, though the velocity would settle
on some value that was less than the target velocity (since it wasn't a direct
relationship, but just a hacked one).

> I'm not sure about this, but I think I see some dissonance in your phrasing 
> about adjusting the voltage so you should somehow need to get voltage as an
> input to the error calculation? Don't dwell on the voltage. It's not an input to the 
> error calc. It's just what you adjust against. It's the job of the measured
> error and the PID constants and calculation to adjust it. If you share your
> actual code we might be better able to help.

The GetMotor() method returns a value of 0.0 to 1.0, which is my input, and
the SetMotor() method sets a value of 0.0 to 1.0.

I've got the standard embarrassment of sharing half-finished code but I certainly
am willing...

Cheers,

Murray

...........................................................................
Murray Altheim <murray18 at altheim dot com>                       = =  ===
http://www.altheim.com/murray/                                     ===  ===
                                                                    = =  ===
     In the evening
     The rice leaves in the garden
     Rustle in the autumn wind
     That blows through my reed hut.
            -- Minamoto no Tsunenobu



More information about the DPRGlist mailing list