• Which the release of FS2020 we see an explosition of activity on the forun and of course we are very happy to see this. But having all questions about FS2020 in one forum becomes a bit messy. So therefore we would like to ask you all to use the following guidelines when posting your questions:

    • Tag FS2020 specific questions with the MSFS2020 tag.
    • Questions about making 3D assets can be posted in the 3D asset design forum. Either post them in the subforum of the modelling tool you use or in the general forum if they are general.
    • Questions about aircraft design can be posted in the Aircraft design forum
    • Questions about airport design can be posted in the FS2020 airport design forum. Once airport development tools have been updated for FS2020 you can post tool speciifc questions in the subforums of those tools as well of course.
    • Questions about terrain design can be posted in the FS2020 terrain design forum.
    • Questions about SimConnect can be posted in the SimConnect forum.

    Any other question that is not specific to an aspect of development or tool can be posted in the General chat forum.

    By following these guidelines we make sure that the forums remain easy to read for everybody and also that the right people can find your post to answer it.

AP speed hold PID controller

Heretic

Resource contributor
Messages
6,830
Country
germany
I know that there are at least half a dozen threads on this subject out there, but the solutions offered are either frontends for FSX autopilot code or the code is fairly hard to get into or is pretty spaghetti-ish.

My last - and only foray - into PID controllers was for half a semester almost seven years ago and the subject is so complex that fully catching up for a flight simulator project is nigh impossible, so forgive me if my terminology and explanations are sketchy.

The code below works rather well for a medium sized passenger aircraft, but might require adaption for any other winged contraptions.

Code:
(A:AIRSPEED INDICATED, knots) (L:AP_SPD_Hold_Target, number) - (>L:AP Speed Diff, number)

(P:Absolute time,seconds) (L:AP Timer 1, number) ==
if{ 1 (>G:Var8) }
els{ 0 (>G:Var8) }
(P:Absolute time,seconds) (>L:AP Timer 1, number)

(G:Var8) 1 <
if{
<!-- Proportional: Trim when outside of airspeed tolerance. Kp = 1, Boundaries: 0.1 to 20 knots (absolute) -->
-20 -0.1 (L:AP Speed Diff, number) rng
if{ (>K:ELEV_TRIM_DN) }
0.1 20 (L:AP Speed Diff, number) rng
if{ (>K:ELEV_TRIM_UP) }

<!-- Integrative: Convert ft/s2 into nm/s2, then project 72 / 18 = 4 seconds into the future. Ki = 4, Boundaries: Projected speed lower/higher than target speed to infinity. -->
(A:ACCELERATION BODY Z, feet per second squared) 0.592484 * 72 * (A:AIRSPEED INDICATED, knots) + (L:AP_SPD_Hold_Target, number) &lt;
if{ (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) }
(A:ACCELERATION BODY Z, feet per second squared) 0.592484 * 72 * (A:AIRSPEED INDICATED, knots) + (L:AP_SPD_Hold_Target, number) &gt;
if{ (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) }

<!-- Derivative: Trim when acceleration is outside of limits. Kd = 2, Boundaries: "Not 0" to inf -->
(A:ACCELERATION BODY Z, feet per second squared) 0 &lt;
if{ (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) }
(A:ACCELERATION BODY Z, feet per second squared) 0 &gt;
if{ (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) }
}

Calculating the airspeed difference is pretty self-explanatory.

During development I've found that running the controller in a timer decreased its effectiveness, so to keep the code running at MSFS' 18Hz limit and still stop execution when the sim is paused, I've introduced the timer-related block as a cheap method for determining the sim's pause status.
The variables for the sim's pause status which are provided by XMLTools or the XML Sound gauge tend to stop working after half a dozen aircraft reloads so I have to stick to a more basic approach.

As far as I know, the proportional element of a controller is used to counteract large deviations from the target value, but is prone to induce unwanted oscillations. Since I've found that it did more harm than good when left unchecked, I've introduced a narrow boundary in which it may influence pitch trim. This is also the reason why the proportional gain on elevator trim is rather low.

Again, as far as my one hour refresher course provided, the integrative term is used to determine a trend to help "steer" the current value toward the target value. The trend is created by reading the current acceleration, convertinng, multiplying and then adding it to the current speed to obtain a future value for the current airspeed. Since this works rather well for dampening controller response, I've made it the most important element of the controller as can be seen by the amount of trim commands issued.

The derivative term basically trolls the other two terms by counteracting their commands to minimize acceleration. Yet, it helps to dampen the control response of the other two terms and thus enable the controller to attaint he target spead more quickly. Therefor I've made this the second most important term in the chain as evidenced by the control response.


When enabling this controller with a huge differential between your target and actual speed, you will see one or two pretty hefty oscillations, but this is quite normal. After that, the oscillations should successively become smaller before settling on a pitch value that will maintain the target speed.
Should the plane not stop oscillating, you will have to play with the boundary conditions, "look ahead" time or the gains (number of control inputs, i.e. "trim up/down").

I hope that my explanation wasn't too wrong and that someone finds this useful.
 
A full PID! I had actually tackled this very same AP mode (use pitch to hold IAS at activation time) and settled on implementing a PID that will alter AP target pitch based on a target speed acceleration. After implementing the proportional part, I stopped, as it just worked well enough! Here the code:

Code:
                <!--IAS PITCH MODE-->
              
                (A:AUTOPILOT AIRSPEED HOLD,bool) if{ 0 (&gt;L:AUTOPILOT_IAS, bool) }
              
                (L:AUTOPILOT_IAS,bool) if{
              
                    <!--acceleration calculator-->
                    (L:ACC_SAMPLE,number) 0 == if{ (A:Airspeed select indicated or true,knots) (&gt;L:ACCEL_UPDATE,number) }
                    (L:ACC_SAMPLE,number) 1 + (&gt;L:ACC_SAMPLE,number)
                    (L:ACC_SAMPLE,number) 18 &gt; if{
                        (A:Airspeed select indicated or true,knots) (L:ACCEL_UPDATE,number) -  (&gt;L:IAS_ACC,number)
                        0 (&gt;L:ACC_SAMPLE,number)
                    }
                    <!--target acceleration calculator-->
                    (A:Airspeed select indicated or true,knots) (L:PITCH_IAS,number) - -10 / (&gt;L:TGT_ACCEL,number)
                  
                  
                    (L:TGT_ACCEL,number) (L:IAS_ACC,number) - 10 * (&gt;L:IAS_PID_P_DELTA,number)
                    (L:IAS_PID_P_INIT,number) (L:IAS_PID_P_DELTA,number) + (&gt;L:IAS_PID_P,number)
                  
                    (L:IAS_COUNT,number) 3 &gt; if{
                        (L:IAS_PID_P,number) (A:AUTOPILOT PITCH HOLD REF,degrees) &lt; if{ (&gt;K:AP_PITCH_REF_INC_UP) }
                        (L:IAS_PID_P,number) (A:AUTOPILOT PITCH HOLD REF,degrees) &gt; if{ (&gt;K:AP_PITCH_REF_INC_DN) }
                        0 (&gt;L:IAS_COUNT,number)
                    }
                    (L:IAS_COUNT,number) 1 + (&gt;L:IAS_COUNT,number)
                }
                els{ 0 (&gt;L:IAS_COUNT,number) }


- It will refuse to engage if Auto-Throttle is ON for obvious reasons.
- When you click the button, it saves current IAS as (L: PITCH_IAS,number).
- first part computes speed change each second (once every 18 cycles) as (L:IAS_ACC,number).
- target acceleration is proportional to error between target and current speed: (L:TGT_ACCEL,number)
- (L:IAS_PID_P_DELTA,number) is proportional error in acceleration
- (L:IAS_PID_P_INIT,number) is current pitch saved at moment of activation. adding (L:IAS_PID_P_DELTA,number) each cycle and saving to (L:IAS_PID_P,number)
- (L:IAS_COUNT,number) allows a pitch command every 4 cycles until (L:IAS_PID_P,number) is reached

Trick here is target is not speed but acceleration!
This is a small/medium airliner too! for different aircraft, you may ned to tune gains for (L:IAS_PID_P_DELTA,number) and (L:TGT_ACCEL,number).
Activating the mode will result in a slight overshoot at first, but by the second oscillation is imperceptible. It responds very well to throttle changes, and seems to allow +-5Kts error. Good enough for me! You can engage at 250KIAS and let the airplane climb to max ceiling with engines at MCT while you relax! Or engage it, cut the throttle, and safely go down at whatever speed you like!
 
To detect pause in my code, I use this code in a common script gauge :
Code:
<KeyMap id="KeyMap">
    <Trigger id="Detect PAUSE ON">
        <KeyEvent>PAUSE_ON</KeyEvent>
            <Script>
                1 (>L:pause_on,bool)
            </Script>
    </Trigger>
    <Trigger id="Detect PAUSE OFF">
        <KeyEvent>PAUSE_OFF</KeyEvent>
            <Script>
                0 (>L:pause_on,bool)
            </Script>
    </Trigger>
</KeyMap>

and I begin each gauge that needs to not run when in pause by a test and a Goto to the end of the gauge:

Code:
(L:pause_on,bool) 1 == if{ g59 }

.....gauge script.....

:59


And about pitch control, I use empiric method :rolleyes: . But my aircraft is at constant thrust (fixed N1 value), so, this is only the pitch control that must get the targert speed. So, I had to divide in seperate gauges to control the climb and the descent, because aircraft has not same behavior.
But result is a +/- 1 knots of precision to follow target speed.

Only convenience, my code is FPS sensitive (if FPS down below 12/15 FPS, gauges not work with the same accuracy).
 
For reference, next to the one presented by Mario in post #2, here are other approaches for implementing a pitch controlled speed hold mode:
http://fsdeveloper.com/forum/threads/xml-ap-ias-hold-by-pitch.90118/#post-257538 - courtesy of Joao Muas
http://fsdeveloper.com/forum/threads/speed-trend-vectors-and-flc-mode-xml.172188/#post-563358 - courtesy of Geoff
http://fsdeveloper.com/forum/threads/speed-trend-vectors-and-flc-mode-xml.172188/#post-705518 - courtesy of François Doré
http://fsdeveloper.com/forum/threads/autopilot-ias-hold-mode.433457/#post-704546 - courtesy of Nicholas Weber

And other examples for XML-Based PID controllers:
http://fsdeveloper.com/forum/threads/ground-speed-control.69176/#post-215160 - taxi speed control by throttle, courtesy of Chris
http://fsdeveloper.com/forum/thread...or-trim-indicator-problem.206666/#post-380998 - AOA hold by Roy Holmes, based on Chris' method


Trick here is target is not speed but acceleration!
This is a small/medium airliner too! for different aircraft, you may ned to tune gains for (L:IAS_PID_P_DELTA,number) and (L:TGT_ACCEL,number).
Activating the mode will result in a slight overshoot at first, but by the second oscillation is imperceptible. It responds very well to throttle changes, and seems to allow +-5Kts error. Good enough for me! You can engage at 250KIAS and let the airplane climb to max ceiling with engines at MCT while you relax! Or engage it, cut the throttle, and safely go down at whatever speed you like!

As far as I know, having only a proportional response to a changed target value will produce a permanent over- or undershoot, which is the 5 knot deviation you're observing.
Not to say that my controller is any better. It will stabilize pitch somewhat below the target speed and then slowly accelerate toward it. I can very well imagine that this is how the real autopilot works as well.

And yes, it's quite fun to play with the throttle to hold your altitude.


and I begin each gauge that needs to not run when in pause by a test and a Goto to the end of the gauge:

Code:
(L:pause_on,bool) 1 == if{ g59 }

.....gauge script.....

:59

I think you can also use the "quit" statement for that, which will stop evaluating the current <update> or the entire gauge (not sure about that).


And about pitch control, I use empiric method :rolleyes: . But my aircraft is at constant thrust (fixed N1 value), so, this is only the pitch control that must get the targert speed. So, I had to divide in seperate gauges to control the climb and the descent, because aircraft has not same behavior.
But result is a +/- 1 knots of precision to follow target speed.

Your code was a bit of an inspiration because it was the first one I've tested which worked reasonably well for my setup. This was a small confidence boost, so thanks for posting it!

"N1 hold" was also used in old aircraft, with the exception that it was done by the pilot instead of the FADEC. :D
You set your climb thrust at 1500 AGL or so and then left the throttle alone until reaching your cruise altitude, using pitch to control speed.
 
Thanks Heretic, and happy if my "amateur" code helped you.

The last version (to descend here) is the following (I simplified and code is now the same for mach and IAS area :
Code:
(L:Descent_constraint,bool) 0 ==
(L:VS_Mode,bool) 1 == and if{
                            0 (>K:AP_N1_REF_SET)
                            (* Baisse et maintien du pitch en mode IAS *)
                            (P:Absolute time,seconds) 1 % 0.04 1 * > ! if{
                                                                        (A:ATTITUDE INDICATOR PITCH DEGREES,degrees) 5 &lt;
                                                                        (A:Airspeed select indicated or true,knots) (L:Speed_Managed,enum) 2 - &lt; and
                                                                        -25 0.25 (A:ACCELERATION BODY Z,feet per second squared) rng and
                                                                        (A:Vertical Speed,feet per minute) -4500 &gt; and if{ 1 (>K:AP_PITCH_REF_INC_DN) }
                                                                     
                                                                        (A:ATTITUDE INDICATOR PITCH DEGREES,degrees) 5 &gt;
                                                                        (A:Airspeed select indicated or true,knots) (L:Speed_Managed,enum) 2 + &gt; or
                                                                        -0.20 25 (A:ACCELERATION BODY Z,feet per second squared) rng and
                                                                        (A:Vertical Speed,feet per minute) -1200 &lt; and if{ 1 (>K:AP_PITCH_REF_INC_UP) }
                                                                     
                                                                        (A:Airspeed select indicated or true,knots) (L:Speed_Managed,enum) 10 - &lt;
                                                                        (A:ATTITUDE INDICATOR PITCH DEGREES,degrees) -4 &lt; or
                                                                        (A:Vertical Speed,feet per minute) -4000 &gt; and if{ 1 (>K:AP_PITCH_REF_INC_DN) }
                                                                     
                                                                        (A:Airspeed select indicated or true,knots) (L:Speed_Managed,enum) 10 + &gt;
                                                                        (A:ATTITUDE INDICATOR PITCH DEGREES,degrees) 5 &gt; or
                                                                        (A:Vertical Speed,feet per minute) -500 &lt; and if{ 1 (>K:AP_PITCH_REF_INC_UP) }
                                                                     
                                                                        (A:Vertical Speed,feet per minute) -400 &gt; if{ 1 (>K:AP_PITCH_REF_INC_DN) }
                                                                     
                                                                        (A:Autopilot Mach Hold,bool) 1 == if{ 1 (>K:AP_PANEL_MACH_OFF) }
                                                                     
                                                                        (A:Vertical speed,feet per minute) -4000 &lt; if{ 1 (>K:AP_PITCH_REF_INC_UP) }
                                                                        }

                            (A:Airspeed select indicated or true,knots) (L:Speed_Managed,enum) 18 + &gt;
                            (A:Autopilot altitude lock,bool) 0 == and
                            (L:DescentPhase,enum) 2 &gt; and
                            (A:Autopilot Airspeed Hold,bool) 0 == and if{ 1 (>L:MoreDrag,bool) } els{ 0 (>L:MoreDrag,bool) }
                         
                            (A:Autopilot Airspeed Hold,bool) 1 == if{ 1 (>K:AP_PANEL_SPEED_OFF) }
                            (A:Autopilot Mach Hold,bool) 1 == if{ 1 (>K:AP_PANEL_MACH_OFF) }
                            }

To help my work (and to find a good formula to calculate the TOD), I created a monitoring gauge during the descent which write data each 6 seconds in a CSV file :
you can see here the gauge working:
136756Image3.jpg
 
Trying to improve the code some more, I found out that if I take vertical (Y axis) acceleration into account, I get a much better response from the controller, to the point that I can cut the amount of trim commands issued by the integrative controller per update. There is still some target speed lapse during climbs or descents, but much less so than before. Responsiveness has also increased.

Code as used in a DC-9:
Code:
<!-- Capture current indicated airspeed (and reset mach variable) -->
(L:DC9_AP_SPD_Target, number) 1 &lt;
if{ (A:AIRSPEED INDICATED, knots) (>L:AP_SPD_Target, number) }

<!-- Calculate deviation from target value -->
(A:AIRSPEED INDICATED, knots) (L:AP_SPD_Target, number) - (>L:Speed_Diff, number)

<!-- Proportional: Trim when outside of airspeed tolerance. Kp = 2, Boundaries: +/-5 knots -->
(L:Speed_Diff, number) -5 &lt;
if{ (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) }
(L:Speed_Diff, number) 5 &gt;
if{ (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) }

<!-- Integrative: Convert m/s^2 into nm/s^2, then look 90 / 18 = 5 seconds into the future. Ki = 3, Boundaries: Projected speed lower/higher than target speed from +/-1 knot deviation onward. -->
(A:ACCELERATION BODY Z, meters per second squared) (A:ACCELERATION BODY Y, meters per second squared) + 1.94384 * 90 * (A:AIRSPEED INDICATED, knots) + (L:AP_SPD_Target, number) - -1 &lt;
if{ (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) }
(A:ACCELERATION BODY Z, meters per second squared) (A:ACCELERATION BODY Y, meters per second squared) + 1.94384 * 90 * (A:AIRSPEED INDICATED, knots) + (L:AP_SPD_Target, number) - 1 &gt;
if{ (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) }

<!-- Derivative: Trim when acceleration is outside of limits. Kd = 3, Boundaries: +/-0.25 m/s^2 to inf -->
(A:ACCELERATION BODY Z, meters per second squared) (A:ACCELERATION BODY Y, meters per second squared) + -0.25 &lt;
if{ (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) (>K:ELEV_TRIM_DN) }
(A:ACCELERATION BODY Z, meters per second squared) (A:ACCELERATION BODY Y, meters per second squared) + 0.25 &gt;
if{ (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) (>K:ELEV_TRIM_UP) }
 
I actually wanted to post some code and explanations about a more true-to life PID controller here, but I found that it didn't work well when the amount of disturbances were too great, e.g. by rapid throttle and simultaneous target speed changes.

One thing that did work rather well, though, was using the elevator as an output variable as it provides a much more rapid response time than elevator trim. Writing to the default EventID ("ELEVATOR_SET" or "AXIS_ELEVATOR_SET") is hardly possible, so one has to use XMLTools' SimVars class instead.

I'm still trying to get a "proper" PID controller, akin to the one used in the default autopilot, to work, but it involves a lot of trial & error, especially when tuning the gain.
 
Last edited:
As announced, the PID controller. Built upon the real PID algorithm with a proportional, integrative and derivative term and adapted to MSFS' input and output variables. The example below is built for stability, but more on that below. Again, note that it's directly writing to the elevator position variable with XMLTools' SimVars class as this yielded the best results during the last seven days that I've worked on it.

Code:
<!-- PID Airspeed hold controller -->
<!-- 1. Pause status detection -->
   (E:Absolute time,seconds) (L:PID AP Time, number) ==
   if{ 1 (>G:Var8) } els{ 0 (>G:Var8) }
   (E:Absolute time,seconds) (>L:PID AP Time, number)
<!-- 2. Main conditional expression; only runs when speed hold is active and the sim is NOT paused -->
   (G:Var8) 0 == (L:AP Speed Hold, bool) and if{
<!-- 3. Disengage altitude hold if enabled, reset elevator trim and assign airspeed target -->
   (A:AUTOPILOT ALTITUDE LOCK, bool) if{ (>K:AP_ALT_HOLD_OFF) }
   (A:ELEVATOR TRIM POSITION, position) abs 0.1 &gt; if{ 0 (>K:ELEVATOR_TRIM_SET) }
   (L:AP_SPD_Target, number) 1 &lt; if{ (A:AIRSPEED INDICATED, knots) near (>L:AP_SPD_Target, number) }
<!-- 4. Determine error (difference between actual speed and target speed). Offset target speed by +4 knots to account for elevator deflection. -->
   (A:AIRSPEED INDICATED, knots) (L:AP_SPD_Target, number) 4 + - (>G:Var1)
<!-- 5. Proportional control. Kp = 0.05 -->
   (G:Var1) 0.05 * (>L:PID AP P, number)
<!-- 6. Integrative control. Integral time = 36 cycles (conversion to acceleration in knots/s!). Ki = 0.025. Output limit: +/- 0.30. -->
   (A:ACCELERATION BODY Z, meters per second squared) 1.94384 * 36 * (G:Var1) + 0.025 * -0.30 max 0.30 min (>L:PID AP I, number)
<!-- 7. Derivative control. (Acceleration in knots/s) Kd = 0.05. Output limit: +/- 0.33. -->
   (A:ACCELERATION BODY Z, meters per second squared) 1.94384 * 0.05 * -0.33 max 0.33 min (>L:PID AP D, number)
<!-- 8. Calculate total response and write to elevator variable -->
   (L:PID AP P, number) (L:PID AP I, number) + (L:PID AP D, number) + (>C:SIMVARS:Elevator Position, position)
<!-- End of main conditional expression -->
   }
<!-- 9. Reset target speed variable and set elevator to zero for better manual takeover -->
   (L:AP Speed Hold, bool) ! if{
   (L:AP_SPD_Target, number) 0 != if{ 0 (>L:AP_SPD_Target, number) 0 (>K:ELEVATOR_SET) }
   }

Code:
1:
The first expression is crude method of detecting whether the sim is paused or not. It's possible to use the appropriate variables from Doug's XML sound gauge or XMLTools, but I find that they always stop working after a few aircraft reloads. You can also save yourself the G:Var, invert the "==" condition to "!=", delete the "els{ }" condition and have the controller run in the "if{ }" expression (keep in mind to place the expression updating the "PID AP Time" outside of the main "if{ }" expression). However you prefer, it is absolutely necessary that the controller does not run in the background when the sim is paused as it will screw the controller value up when unpausing!

2:
The main "if{ }" conditional expression only executes when the sim is NOT paused and the speed hold mode is engaged.

3:
Next, the altitude hold mode of the autopilot will be disengaged (since airspeed hold mode trades altitude for speed) and the trim will be reset to avoid undue trim interference. The current airspeed will be written to a target variable.

4:
Continuously determine the current error, which is the difference between current airspeed and the target airspeed. In real PID theory, it's the other way around but the controller won't have to deal with negative gain values this way and will output more logical values (negative error: too slow -> negative elevator input -> pitch down, positive error: too fast -> positive elevator input -> pitch up). This expression may very well be integrated into the folllowing P and I term, but keeping it separate helps with readability. Note the "4 +" which will offset the target speed by 4 knots and whose function will explained in the "tuning" section.

5:
The proportional control of the controller scales the response to the deviation from the target value, with greater differences resulting in greater pitch commands. Imagine the P control as the brute force part of the controller that will counter any deviation with a proportional reaction. To scale the influence of the P control, adjust the proportional gain (Kp; 0.05 in the example above). When running on its own, a proportional controller will induce pitch oscillations that may very well lead to a loss of of control.

6:
Integrative control anticipates the change of the error (difference between current and target airspeed) over time. It provides a glimpse into the future to adjust elevator output to minimize the error and thus steer the controller from the current airspeed to the target airspeed. Consider it the "brain" of the controller. The future error is calculated by integrating the error over a certain time interval. In this case, the current acceleration (in meters/second^2) is first converted into knots/second by multiplication with 1.94384 (since the error uses knots as a unit) and then scaled by 36, which, considering MSFS' 18 cycles/second gauge refresh rate, corresponds to a 2 second integration time. The resulting change in velocity due to acceleration is then added to the current error to obtain the projected error in 2 seconds. Output may be scaled with Ki, which, in this case, is 0.005. To avoid excessive outputs in some flight conditions, integrative output is limited to a range from -0.30 to 0.30 (which corresponds to 30% of the available elevator range in a single direction). Mind MSFS' peculiar way of describing minimums as maximums and the other way around (a property of dealing with stacks; use the forum search if you need an explanation).

7:
Without derivative control, a controller tends to oscillate around the target value with the proportional and integrative control fighting about attaining their respective targets. This is especially true in a highly dynamic system like an aircraft where lots of disturbances will influence airspeed. Therefor, the controller needs derivative element that basically "calms" the other two controls down and strives to minimize the rate of change. Since the rate of change for aircraft speed is acceleration (a derivation of speed by time), acceleration is this control's input. Since the default unit for acceleration in FSX is m/s^2, a scaling factor has to be applied to change the output into knots per second. Output gain may be scaled with Kd, which is 0.05 in the code above. D output is limited to +/- 0.33 to avoid excessive elevator movement in some flight conditions.

8:
Total response from the controller is calculated by summing up the proportional, integrative and derivative response and writing it to the elevator position variable using XMLTools' SimVars class.

9:
Reset the target speed variable and elevators (for better manual takeover) when "speed hold" is disabled.


Tuning:
Tuning PID controllers is a science in itself (just google the subject and marvel at the results), but I found that all the scientific tweaking methods don't quite work in the flight simulator scenario. So you're left with the good old "trial & error" method and a some basic rules of thumb and considerations regarding PID control. First, we need to talk about limitations, which basically are:

  • Update rate. Unless implemented into an aircraft model, the PID controller runs in a 2D gauge running at 18 Hz (18 updates/second). When the rest of the simulator runs at higher refresh cycles (20 FPS, 30 FPS, 60 FPS or - worse - unlimited), reaction time to disturbances and controller output will degrade. Real PID controllers run near or at real time and thus can do a much better job in reacting to changed target values and process disturbances.
  • Amount of disturbances. Airplanes are highly dynamic systems with lots of influences, which can range from turbulence, change in drag due to airfoil aerodynamics and changes in thrust. Just when the controller has settled in and is in the process of steering the aircraft to its target speed, a sudden power burst or wind gust can throw it off again. Coupled with the limitation in reaction time mentioned above and less than optimal tweaking, you might end up with an oscillating output or - worst case - total loss of airplane control. So proper tweaking and lots of testing in a lot of situations is quite important.

Ideal PID controllers possess the following properties:
  • Sensitivity to quickly react to disturbances and changing target values
  • Accuracy to always attain the target value
  • Stability to cancel out any disturbances to the process
Each of these properites corresponds to one of the three controls described above.
  • P control makes the process sensitive to change as any deviation from the target value is countered with an instant reaction, but at the cost of overshoots and oscillations in a FSX aircraft for the two limitations stated above.
  • I control increases accuracy as it tries to minimize the error through look-ahead and appropriate scaling of the output (elevator position). It, however, suffers from the same problem as P control in terms of susceptibility to oscillations.
  • D control increases process stability as it will actively try to reduce the rate of change. Is used - to a certain degree - as a remedy for oscillations in potentially unstable processes (such as aircraft).
Due to these properties, using either of these controls on its own is out of the question. P or I controls would end up oscillating while D control would make the aircraft not accelerate or slow down at all, regardless of the target value. A PI control won't work either, but a PD control with a strong enough D control gain will do in some cases at the cost of settling anywhere near (but not at) the target value when the P control can't overcome the D control's tendency to minimize acceleration. Therfor, the only option for a speed hold is a combined PID control.

With that in mind, you can tweak the P, I and D gains to obtain the following:
  • Sensitivity and accuracy with low stability: High P and I gains, low D gain, no output limitations.
  • Low sensitivity and accuracy with high stability: Low P and I gains, high D gain, limited I and D outputs.
  • A compromise between all three properties. This is what you should aim for when tuning the controller.

While tuning the controller, it is very important to monitor your outputs, namely:
  • Current speed
  • Target speed
  • Difference between current and target speed
  • P control
  • I control
  • D control
  • Sum of the P, I and D control
  • Elevator position

That way, you can easily spot just where the controller doesn't work as it's supposed to.

Don't increase the gain for any of the three controls too much as each relies somewhat on the other controls to be kept in check and, in the case of D control will often result in very strong initial controller output (and thus elevator movement) after loading the aircraft in flight. There's also no "one size fits all" solution and the gains and output limits will have to be adjusted on a per-aircraft base. The code example above is tuned for a small (overpowered) business jet and emphasizes stability to minimize the influence of disturbances like rapid full range throttle changes (from idle to full or vice versa). It isn't perfect by any means since there's a bit of a deviation from the target speed when climbing at maximum throttle or descending as well as some oscillation during great variations in throttle, but it works well enough to be usable.

The target speed offset mentioned in the code description (the "+ 4" or "4 +" in MSFS' reverse polish notation) is mainly used for cosmetic purposes to avoid displaying the steady-state error in your monitoring gauges or flight instrumentation. Said steady-state error is introduced by directly writing the PID output to the elevator position as, in most cases, the airplane will require a certain elevator deflection to maintain a certain speed at a constant power setting. Therefor, to obtain the required elevator deflection from the P and I controls (D is out of the equation since the plane doesn't accelerate), an error has to be introduced which is done by applying a fixed offset.
It may sound like heresy, but after all the time I've spent developing the controller, it was the best solution. The alternative, which I've used for a long time, was applying the PID output as an offset to the last elevator position (elevator_old + PID offset = elevator_new), but this was very prone to instability (regardless of individual control gains) for the very limitation of the gauge refresh rate. The steady-state approach will introduce some target value deviation in some conditions, but is much more stable as it is the most direct approach to get the output to the simulator (18Hz gauge refresh rate!).
To determine the value of the steady-state error for your airplane, remove the offset from the code (or set it to zero, i.e. "0 +"), then put your airplane into speed hold and cancel out any vertical speed with the throttle. When the VSI is at or very near 0 ft/min, note the error (in my case, it was -4 KIAS, i.e. 4 knots short of the target speed), apply it to the error in the code and retest. The numbers in your control gauge or autopilot instrumentation will look much better.


Hope this wall of text is of use.

P.S:
Mach hold is a different animal, but can be attained with lower gains or by upscaling the error.

P.P.S:
You may also use the default (>K:ELEVATOR_SET) variable to write the elevator position as long as you apply a scaling factor of "-16383" to the PID output.
 
I've put the controller into a Sperry 50 autopilot (from a DC-9) and only needed to edit the target speed offset and the autopilot switch variables. No further adjustments needed. Pretty cool.



The controller can even be adapted for mach hold fairly easily. Most changes regard input and target variables and their value range.

Code:
<!-- PID Mach hold controller -->
<!-- 1. Pause status detection -->
   (E:Absolute time,seconds) (L:PID AP Time, number) ==
   if{ 1 (>G:Var8) } els{ 0 (>G:Var8) }
   (E:Absolute time,seconds) (>L:PID AP Time, number)
<!-- 2. Main conditional expression; only runs when speed hold is active and the sim is NOT paused -->
   (G:Var8) 0 == (L:AP Mach Hold, bool) and if{
<!-- 3. Disengage altitude hold if enabled, reset elevator trim and assign airspeed target -->
   (A:AUTOPILOT ALTITUDE LOCK, bool) if{ (>K:AP_ALT_HOLD_OFF) }
   (A:ELEVATOR TRIM POSITION, position) abs 0.1 &gt; if{ 0 (>K:ELEVATOR_TRIM_SET) }
   (L:AP_Mach_Target, number) 0.001 &lt; if{ (A:AIRSPEED MACH, mach) (>L:AP_Mach_Target, number) }
<!-- 4. Determine error (difference between actual speed and target speed). Offset target speed by -0.004 mach and scale output by 1000 to account for elevator deflection. -->
  (A:AIRSPEED MACH, mach) (L:AP_Mach_Target, number) 0.004 - - 1000 * (>G:Var1)
<!-- 5. Proportional control. Kp = 0.05 -->
   (G:Var1) 0.05 * (>L:PID AP P, number)
<!-- 6. Integrative control. Integral time = 36 cycles (conversion to acceleration in knots/s!). Ki = 0.025. Output limit: +/- 0.30. -->
   (A:ACCELERATION BODY Z, meters per second squared) 1.94384 * 36 * (G:Var1) + 0.025 * -0.30 max 0.30 min (>L:PID AP I, number)
<!-- 7. Derivative control. (Acceleration in knots/s) Kd = 0.05. Output limit: +/- 0.33. -->
   (A:ACCELERATION BODY Z, meters per second squared) 1.94384 * 0.05 * -0.33 max 0.33 min (>L:PID AP D, number)
<!-- 8. Calculate total response and write to elevator variable -->
   (L:PID AP P, number) (L:PID AP I, number) + (L:PID AP D, number) + -16383 * (>K:ELEVATOR_SET)
<!-- End of main conditional expression -->
   }
<!-- 9. Reset target speed variable and set elevator to zero for better manual takeover -->
   (L:AP Mach Hold, bool) ! if{
   (L:AP_Mach_Target, number) 0 != if{ 0 (>L:AP_Mach_Target, number) 0 (>K:ELEVATOR_SET) }
   }

Notes:
  • The input variable is scaled by 1000 to bring it into the same value range as for regular speed hold (saves some work trying to figure out new P,I and D gains).
  • A Mach target offset is again used to account for elevator position.
  • To demonstrate a XMLTools-less variant of the controller, the ouput is written to the standard elevator key event.


- Edit:

I've tested a version with proper input variables for the integrative control (mach acceleration instead of standard acceleration) and tried various gain settings. None worked as well as the simple adaption of the speed hold code above. Go figure.
 
Last edited:
I have to revoke my verdict on elevator trim as I found out that writing directly to the elevator itself produces some ugly twitching in some situations which will translate directly to the entire airplane. This is not the case when using trim as an output because it's a second order influence, so things will be a lot smoother.

Modding the controller for trim is very easy. Surprisingly, all the gains work just as well and the only parameters requiring adjustment are the target speed offsets to account for steady state errors.

First, remove the line that's resetting trim during every update cycle as it's very obviously counterproductive in this scenario:
Code:
(A:ELEVATOR TRIM POSITION, position) abs 0.1 &gt; if{ 0 (>K:ELEVATOR_TRIM_SET) }

Second, turn the output scalar into a positive value and change the output variable.
Code:
... 16383 * (>K:ELEVATOR_TRIM_SET)

And that's it.


By now, I have the PID controller running in a Learjet, DC-9 and 727 with the only difference being the way it's implemented into the autopilot logic and the target speed offsets.
In each case, flying in speed hold mode is quite fun, with the throttles used to control rate of climb. Perfect for stabilizing the aircraft before handoff to altitude hold mode during cruise.
 
This is genius Bjoern, Thanks a lot for the insight and detailed explanation.
I've implemented this in my Hondajet and spent some time tuning it, Still some tuning left but I have it relatively stable and responsive now.
I've also tried using vertical speed as control variable (>K:AP_VS_VAR_SET_ENGLISH) instead of trim,But as vertical speed is in itself controlled by an internal PID the results of stacking 2 PID controllers was quite unstable for me. Also tried to use pitch (AP_PITCH_REF_INC_DN/UP) but found no way to actually influence the value input to the pitch,It was either increasing/decreasing at constant rates so essentially more of an on/off controller that is simply oscillating around the set point.
Using trim ala your method however is right on the money! Only issue is that the flight director cannot react to it, do you have any ideas how to have the flight director command pitch changes inline with the direct control of the trim?
 
This is genius Bjoern, Thanks a lot for the insight and detailed explanation.
I've implemented this in my Hondajet and spent some time tuning it, Still some tuning left but I have it relatively stable and responsive now.
I've also tried using vertical speed as control variable (>K:AP_VS_VAR_SET_ENGLISH) instead of trim,But as vertical speed is in itself controlled by an internal PID the results of stacking 2 PID controllers was quite unstable for me. Also tried to use pitch (AP_PITCH_REF_INC_DN/UP) but found no way to actually influence the value input to the pitch,It was either increasing/decreasing at constant rates so essentially more of an on/off controller that is simply oscillating around the set point.

Vertical speed hold and pitch hold are actually more popular approaches to simulate speed-by-pitch, but they require careful and very explicit integration with the other default autopilot modes.
I guess with careful tuning of my controller, you can define one of the two variables (VS_VAR or PITCH_REF) as outputs, but I guess that the former can only be tuned in steps of 100 fpm and the latter in steps of 1°, which might be too coarse. Writing to either variable would have the advantage of better interaction with the flight director, which...

Using trim ala your method however is right on the money! Only issue is that the flight director cannot react to it, do you have any ideas how to have the flight director command pitch changes inline with the direct control of the trim?

...in my controller will have to be edited to implement a custom control for the pitch bar. I don't have much of an idea how such a control would look like in terms of input and code though.

In any case, you're welcome to experiment further with the controller.
 
Thanks for the quick reply Bjoern,
Do you have an idea if there is more control on PITCH_REF up or down? They seem to disregard any value input to them completely. pitch reference cannot be set directly by a value? feels controlling pitch targets are the most logical way to go about a FLC mode, or atleast I can have the FD bars on their own PID for manual flight, While the AP controls the trim directly (They should eventually coincide as they are trying to achieve the same thing), Only issue is my lack of control on the FD pitch reference so far.
 
Hi,

Would you happen to have similar code for a PID controller to replace the FS NAV mode of the AP? Many of my planes rock back and forth a bit, and I would like to reduce/eliminate that. I was able to replace the HDG hold mode starting with some code that I found in an old AP. But that does not use a PID, it is pretty much direct. I have tried using similar code for the NAV hold which works pretty well, but I'm curious if it could get better.

Thanks,
 
Thanks for the quick reply Bjoern,
Do you have an idea if there is more control on PITCH_REF up or down? They seem to disregard any value input to them completely. pitch reference cannot be set directly by a value? feels controlling pitch targets are the most logical way to go about a FLC mode, or atleast I can have the FD bars on their own PID for manual flight, While the AP controls the trim directly (They should eventually coincide as they are trying to achieve the same thing), Only issue is my lack of control on the FD pitch reference so far.

None of the related "A:" vars of the pitch reference bars can be directly written to and there's no "_SET" type event, so the increment/decrement events are all you have.
If you have a pitch trim indicator in the cockpit, you can experiment with matching trim position to the pitch bar for a simple solution. Elevator trim would drive a reference L: variable to be used by the pitch bar. If the A: pitch bar variable deviates from the L: var target value, you'd have to issue the appropriate increment/decrement event to until it matches the reference.
Another PID controller to determine the target pitch bar value would be the better solution, but will require lots of work to tune it right.

If this sounds like a lot of effort, rest assured that it is. That's why I haven't investigated it yet, especially since the result is purely cosmetical.


Hi,

Would you happen to have similar code for a PID controller to replace the FS NAV mode of the AP? Many of my planes rock back and forth a bit, and I would like to reduce/eliminate that. I was able to replace the HDG hold mode starting with some code that I found in an old AP. But that does not use a PID, it is pretty much direct. I have tried using similar code for the NAV hold which works pretty well, but I'm curious if it could get better.

Thanks,

Do the aircraft rock when starting the nav intercept or while established on course toward the navaid/waypoint?

The rocking while starting the intercept turn could maybe be improved in a similar way as in the HDG controller by directly controlling aileron movement until a certain bank angle has been attained before handing control over to MSFS' nav hold mode.
Rocking when established and following the signal should be controlled with MSFS' internal nav PID controller (in the aircraft.cfg) though, as it runs at sim framerate and is able to react to disturbances much more quickly.
I figure that rocking in either of both situation might be caused by too much proportional control and/or too little derivative control in the controller.

In any case, and this also applies to custom-coded PID controllers, the gains and limits will have to be adapted on a per-aircraft basis to adjust for differen flight characteristics.
 
Hi,

My current test condition for the NAV mode is to use a 30 deg. intercept at 30 NM distance from the VOR. Using FS NAV mode the plane turns toward the VOR, overshoots, and requires a turn back to the VOR. There is then a tiny overshoot in the opposite direction that requires a tiny turn back to the VOR. I'm now at around 20 NM distance. It then lines up on the VOR and remains steady. The initial turn is rather sudden and quite steep.

With a two stage direct aileron control coding I can completely eliminate both overshoots and be on course at 25 NM from the VOR. I guess that's as good as I'll get. The only negative is that the ailerons are a little "flippy" at certain stages of the intercept, switching between left and right turns to moderate the intercept course.
 
Last edited:
You're probably getting better results at NAV intercept with direct aileron control because you're initially applying smoother gains to the control surfaces than the default NAV hold autopilot would do.

The aileron flapping is normal if your control code only works with a precise target value (e.g. 20°) instead of an upper and lower boundary (e.g. 19° to 21°), since said precise value can never be attained.

And don't forget that any controller will have to be tuned for the aircraft in question to obtain the best result. What works for a DC-6 won't necessarily work for a DC-3.

I still maintain that the best course of action is tuning the PID values in the aircraft.cfg. If the initial response is too great and if there are overshoots, decreasing proportional control (nav_proportional_control) and increasing integrative control (nav_integrator_control) might provide better behavior. Less brute force aileron deflection due to a large deviation and more anticipation of the targat value due to an increase in integrative control.

Besides, I wouldn't want to go for too precise intercepts. For all I know, early autopilots weren't exactly masters at their trade, so a bit of overshoot and correction might just be realistic.
 
Hi Bjorn ,
Wanted to share an approach I've been experimenting with for the integrator term with good success.

Basically the integrator term is the summation of the errors within a past specific time frame multiplied by the time step. So I have a code that stores the error values of the past 5 frames and calculates the cumulative error during that period. I made this diagram to explain the idea better.
PID DIAGRAM.jpg


Code:
<!-- Cycle through each of the errors and update an error every frame , Each error is updated once every 5 frames in sequence  -->
(L:HJ_WhichSpeed,enum) 1 + (>L:HJ_WhichSpeed,enum)

(L:HJ_WhichSpeed,enum) 1 == if{ (L:AirSpeed2,enum) (>L:AirSpeed1,enum) }
(L:HJ_WhichSpeed,enum) 2 == if{ (L:AirSpeed3,enum) (>L:AirSpeed2,enum) }
(L:HJ_WhichSpeed,enum) 3 == if{ (L:AirSpeed4,enum) (>L:AirSpeed3,enum) }
(L:HJ_WhichSpeed,enum) 4 == if{ (L:AirSpeed5,enum) (>L:AirSpeed4,enum) }
(L:HJ_WhichSpeed,enum) 5 == if{ (A:AIRSPEED INDICATED, knots) (L:AP_SPD_Target,number) - (>L:AirSpeed5,enum) }

(L:HJ_WhichSpeed,enum) 5 &gt; if{ 0 (>L:HJ_WhichSpeed,enum) }


<!-- Calculate commulative error = Summation individual errors of the past 5 frames -->
(L:AirSpeed1,enum) (L:AirSpeed2,enum) + (L:AirSpeed3,enum) + (L:AirSpeed4,enum) + (L:AirSpeed5,enum) + (>L:HJET_CummulativeSpeed,enum)



<!-- Calculate integral term by multiplying the commulative error by equivalent time in seconds of 5 frames (0.278) and multiplied by Ki (equals 240 in for my specific aircraft) -->

(L:HJET_CummulativeSpeed,enum) 0.278 * 240 * (>L:PID AP I, number)

I also had very good success feeding the PID output to (>K:AP_VS_VAR_SET_ENGLISH) with alt hold active. The setup is stable and responsive enough that it holds the selected IAS within a very good margin of error in Active Sky's hurricane weather.
 
Last edited:
Not sure that I'd want to spend the extra lines on code that basically calculates the acceleration from the velocity difference. And that I'd want to output the overall result to the autopilot's PID controller.
But as long as it works for you...
 
Last edited:
Back
Top