With the ground firmware working well on a few test vehicles, I've moved back to the air firmware for the FFv1.1 motor controllers. (My motor controller page has finally been updated!) The challenge for the past week was to get the air firmware working in closed-loop RPM mode on the Talon quad, which has nice new motors and a new flight controller for testing.
Closed-loop RPM means that the input signal, a servo-style PWM pulse, commands a specific motor speed. (For example, 1200-1800μs could be mapped to 0-5000rpm.) In RC helicopter terminology, this is called "governor mode" and would be used to hold the rotor speed constant as the blade pitch is varied. It's not typically used on multirotors, since they have fixed-pitch propellers. But there are some performance advantages if it works well. It's also an interesting control system challenge, so I had to try it.
I had previously demonstrated closed-loop speed control with the large hexrotor motors, but the performance was very sluggish and at the time, I decided to use normal voltage (really duty cycle) control for the first test flight. It's the method that most commercial BLDC speed controllers use, and it's reasonably fast, limited only by the ESC's internal filtering. It's also a good approximation to RPM control since applied voltage is roughly proportional to motor speed in steady-state. (IR drop causes some non-linearity.)
The downsides of direct duty cycle control are that it offers no chance to limit motor current and it can't compensate for IR drop or slight differences in motors. Most of the multirotors I've used have cheap motors, and there's always one that's just a little worse than the others... Closed-loop RPM control can solve this problem by ensuring that the same command causes all the motors to spin at the same speed.
Since I already have a current control loop from the ground firmware, adding an outer RPM control loop seemed like it would be easy. This was the loop structure I used in my first closed-loop RPM post:
Motor speed is measured by the flux observer and compared to the desired motor speed to create an RPM error. The error is sent through a PID controller and the output is the demanded current. While there's nothing wrong with this scheme, it happens to be fairly slow. There must be an RPM error in order for the PID controller to have an effect, so it's always chasing the target. To achieve faster performance, I added a feedforward term:
The feedforward term goes from RPM command directly to the current command, bypassing the PID (or in this case, PI only) controller. In steady state, motor current is proportional to the square of motor speed, due to the aerodynamic drag on the propeller. The target current is therefore set to the expected steady state current for a given RPM command, plus a P.I. term to handle transient (accelerating) current and any offset from the expected value. The constant of proportionality for the feedforward path is determined experimentally.
The speed-squared relationship of aerodynamic drag has other consequences for the closed-loop controller: 1) The damping ratio of the plant increases with speed. and 2) The damping ratio is different for increasing and decreasing speed steps. To deal with these effects, I made the gains for the outer PI loop speed-dependent and asymmetric. I don't like speed-dependent gains, but in this case it seems unavoidable for achieving good performance over a large speed range. Higher speeds get higher gains. Increasing speed gets a higher gain than decreasing speed.
The new feedforward structure, combined with speed-dependent gains for the RPM controller, improved the performance a lot. As a baseline, I implemented this scheme on the new Talon motors and tuned the PI gains to get the RPM step response as fast as possible.
|::clamps quadrotor to nearest heavy object::|
To confirm this baseline, I put all the gains and plant specifications into a simulation of the controller/motor/prop system. What I found was that the real-life system was oscillating a lot more than the simulation, with the same gains. That meant there was an unaccounted-for bandwidth limit in the real controller. I spent a day tracking it down. I had already accounted for both the current and the RPM measurement filters, so it had to be something else.
The bandwidth-limiting element turned out to be the high-speed position filter I added a while ago. Above a certain speed, it merges the interpolated electrical angle with the angle indicated by a flux observer zero-crossing. At constant speed, the error between these two angles should be zero mean and the filter doesn't cause any lag. But during acceleration, the interpolated angle will lag the true angle by some amount. This lag was causing oscillation in the D-Axis current controller, which in turn was limiting the overall closed-loop bandwidth.
The solution was simple: I turned the high-speed position filter off. It seems not to be necessary now, possibly because I fixed the problem of flux offset at high speeds. Whatever the reason, the oscillation went away and the real step response looked a lot closer to what the simulation suggested it should (Plot #2). From that point, I made a few small changes to push to faster rise time:
- Changed the RPM estimator from updating once every mechanical cycle (7 electrical cycles) to once every 3 electrical cycles. This decreases the effective low-pass filter time constant in the RPM feedback path. The tradeoff is RPM resolution at high speeds, but 3 cycles seems like a reasonable compromise. Result of this change is Plot #3. The small overshoot in Plot #2 is eliminated.
- Doubled the gains. The result was a slight increase in rise time, but the overshoot also returned (Plot #4). It was clear that increasing the gains more would bring back oscillations.
To improve the rise time (and the closed-loop bandwidth) further, I could speed up the inner current loop by increasing its gains or decreasing the time constant of its low-pass filtering. But the current measurements are noisy and having the filtering in place helps keep sensor noise from being amplified. (It's the classical controls trade-off between closed-loop bandwidth and noise rejection.)
I chose a different option, which was to add another feedforward path. This one goes directly from RPM command to voltage (duty cycle) output:
In some sense, this feedforward path is exactly the direct duty cycle control from before. A step increase in RPM immediately increases the duty cycle to the value that would be expected to produce that RPM in steady state. The output doesn't have to wait for the RPM or current error to build up, so the gains of the feedback controllers can be kept lower.
There is a downside, which is that the feedforward path bypasses the current limit. In the case of a large step increase in RPM command with a low-resistance motor, a short burst of current over the limit could occur before the PI controller has a chance to compensate for it. Depending on the system, a slew-rate limit on the feedforward path could be used to prevent this.
Plot #5 shows the effect of the RPM-to-voltage feedforward path. The 0-100% rise time is cut to under 100ms with acceptable overshoot and very little oscillation. The Q-Axis current instantly rises, rather than building up, giving a short burst of torque to speed up the motor. In this case, the current pulse seems within reason for what the MOSFETs can handle (although what's plotted is filtered current - the instantaneous value on the time scale of one PWM cycle is higher).
Satisfied with the step response, I loaded the new closed-loop control mode onto four FFv1.1's and set them up on the Talon quad:
And here is some data from the indoor test flight showing RPM command tracking:
Despite being very heavy (1.375kg), it flies nicely with the new motors, closed-loop RPM control, and new flight controller. It won't stay in this configuration, since it's too heavy to carry the GoPro now. But it will make a great test platform for the smaller FFv1.2s controllers that I have sitting on my desk waiting to be soldered. As for the larger FFv1.1's, they're most likely going back on the CineStar 6 next...