Howto:Design an autopilot
Note This page is out of date, and does not demonstrate the best way to construct a stable autopilot. |
Note Whenever possible, please refrain from modeling complex systems, like an FDM, autopilot or Route Manager with Nasal. This is primarily to help reduce Nasal overhead (especially GC overhead). It will also help to unify duplicated code. The FlightGear/SimGear code base already contains fairly generic and efficient systems and helpers, implemented in C++, that merely need to be better generalized and exposed to Nasal so that they can be used elsewhere. For example, this would enable Scripted AI Objects to use full FDM implementations and/or built-in route manager systems.
Technically, this is also the correct approach, as it allows us to easily reuse existing code that is known to be stable and working correctly, . For details on exposing these C++ systems to Nasal, please refer to Nasal/CppBind. If in doubt, please get in touch via the mailing list or the forum first. |
The FlightGear forum has a subforum related to: Autopilot & Route Manager |
Autoflight |
---|
Autopilot |
Route manager |
Specific autopilots |
Miscellaneous |
- For a technical reference of the used elements, see Autopilot configuration reference.
This howto will guide you through the process of designing an autopilot that is stable and fully functional. We won't cover every single aspect, because there are hundreds of aircraft out there, with dozens of different autopilot configurations. However, after reading this howto, you should be able to be creative and create new configurations.
Pre-design
Why not make things slightly easier, before we start working on the autopilot? Since we will be updating the autopilot hundreds of times, making tiny adjustments each time, we don't want to reload FlightGear every time. Therefore, we assign an "reload autopilot configuration" action to a key on our keyboard. We've chosen ⇧ Shift+F5, but it could be any key. Add the code to your $FG ROOT/keyboard.xml file.
<key n="261">
<name>F5</name>
<repeatable type="bool">true</repeatable>
<mod-shift>
<desc>Reload autopilot configuration</desc>
<binding>
<command>nasal</command>
<script>
fgcommand('reinit', props.Node.new({ subsystem: "xml-autopilot" }));
print("xml-autopilot reinitialized!");
</script>
</binding>
</mod-shift>
</key>
Creating the autopilot configuration file
Create an autopilot.xml file with the following content, somewhere inside your aircraft's root (eg. Aircraft/747-400/Systems/autopilot.xml).
<?xml version="1.0"?>
<PropertyList>
</PropertyList>
The flightgear xml based autopilot system can run at FDM rate or at frame rate. Using a <autopilot> tag in your aircraft's -set.xml adds the autopilot at fdm rate, using a <property-rule> runs the same config at frame rate.
Adding the autopilot to the -set.xml file
In order for your aircraft to use the configuration file of the autopilot you are about to design, you need to add the path to the file within the <systems> tags in turn within the <sim> tags in your <aircraft>-set.xml file:
<!-- ... -->
<sim>
<!-- ... -->
<systems>
<!-- ... -->
<autopilot>
<path>Aircraft/747-400/Systems/autopilot.xml</path> <!-- Using the the directory in the example above -->
</autopilot>
<!-- ... -->
</systems>
<!-- ... -->
</sim>
<!-- ... -->
A first controller: wing leveler
Let's start with a simple wing leveler that keeps your bank angle at zero (or any arbitrary value later). This could be a PID controller with the input from your current roll and a reference of zero. Output should go to your aileron-cmd. Start with:
- Kp: 0.00
- Ti: 100.00
- Td: 0.00
Be sure to add an <enable> element to easily enable/disable your controller if it gets unstable.
<pid-controller>
<name>Heading hold</name>
<debug>false</debug>
<enable>
<prop>/autopilot/locks/heading</prop>
<value>wing-leveler</value>
</enable>
<input>
<prop>/orientation/roll-deg</prop>
</input>
<reference>0</reference>
<output>
<prop>/controls/flight/aileron</prop>
</output>
<config>
<Kp>0.00</Kp>
<Ti>100.0</Ti>
<Td>0.0</Td>
<u_min>-1.0</u_min>
<u_max>1.0</u_max>
</config>
</pid-controller>
Since this is our first controller, we will give a short explenation of each part.
- Name: is only used for debugging.
- Debug:
- Enable: when the condition is true, the controller will be activated. In this case that will be when /autopilot/locks/heading=wing-leveler.
- Input: is the property that will be monitored by the controller. As our aim is to have a roll of zero degree, our input property is our roll orientation.
- Reference: most of the time this is a property, like input, but for this one we'll use a static value. The controller will try to get the value of the input to this reference-value.
- Output: the controller controls flight surfaces, in order to get the input value to the reference one.
- Config: this is the tricky part. All these settings affect the behaviour (response time, surface deflection etc.) of the controller.
- u_min: the minimum output.
- u_max: the maximum output.
To get an idea of the Kp-value, you need to do a simple calclulation. The PID controller computes "reference - input". If you want wings leveled (zero roll) and your current bank angle is, say 30°, the result of "reference - input" is -30. If you think you need full aileron deflection to get from 30 degrees bank to zero in a reasonable time, you need a factor of 1/30=0.033, because aileron-cmd is in the range of -1 to +1. This is the approximate value for your Kp.
Now enter your aircraft and climb to cruise altitude and cruise speed. Bank your aircraft, to 15°-20°. Enable your controller (in our example that is by setting /autopilot/locks/heading=wing-leveler. Look here if you need help on setting properties.) and - nothing happens (because Kp is still zero). Set the value of Kp to 0.01 and hit Shift-F5 to reload your configuration and check what happens. Does the controller tries to level the wings or do things go worse? If the ailerons move in the wrong direction, you need to reverse the sign of Kp.
Once you found the correct sign for Kp, slowly (in steps as little as 0.005) increase the value of Kp to get a fast response. Don't forget to reload the config with Shift-F5 after every edit of your file. There is one pitfall that can drive you crazy: the PID-controller computes delta-values for its correction and it starts with the current setting of your aileron when it is enabled or reloaded. If you have your aileron set to -0.2 when you hit Shift-F5, the controller will apply its corrections to a value of -0.2 which will most certainly fail! Make sure that you have your flight controls neutral (press 5) when (re-)enabling PID-controllers!
You will achive good results by only setting Kp for a roll-computer as a wing-leveler. Slowly increase your Kp until it is just about to become unstable, than divide the current value of Kp by two and use that value. Try several configurations of your aircraft - slow flight, low altitude, fast cruise at higher altitudes. Fly turns with several bank angles, left and right. Whenever you enable your controller, it should level your wings within a few seconds and without bouncing back and forth.
Once you are satisfied with Kp - and not before! - decrease the value for Ti by dividing by a value of 10 and check the response of your controller. When it tends to become unstable, multiply it's Ti by two. Now you should have a fast and stable wing-leveler.
Finally (for the wing-leveler) you can turn it into a roll-hold by setting your target roll as a property for the reference. If you had <reference>0.0</reference>, bind your reference to a property (<reference>autopilot/settings/target-roll-deg</reference>) and your aircraft should roll to any value you set in target-roll-deg. The complete wing-leveler will look similar to the one below (though, the config values may vary and the reference might be replaced by a property).
<pid-controller>
<name>Heading hold</name>
<debug>false</debug>
<enable>
<prop>/autopilot/locks/heading</prop>
<value>wing-leveler</value>
</enable>
<input>
<prop>/orientation/roll-deg</prop>
</input>
<reference>0</reference>
<output>
<prop>/controls/flight/aileron</prop>
</output>
<config>
<Kp>0.015</Kp>
<Ti>10.0</Ti>
<Td>0.0</Td>
<u_min>-1.0</u_min>
<u_max>1.0</u_max>
</config>
</pid-controller>
This is the base for any lateral hold mode, so try to spend some time on it. It can take some hours if this is your first autopilot.
Pitch hold
So, let's move over to a pitch hold. Our goal is to have a controller that maintains an arbitrary pitch. The only difference to the wing leveler is that you are only interested in elevator position changes, not absolute values. To put it in other words: increase elevator for more pitch, decrease for less pitch and maintain elevator position if the pitch is right. This calls for a pid-controller, because it only computes offset values. And you will end up with a relative small value for Kp to avoid rapid imediate movements and a relative small value for Ti to have a greater effect over time.
Start again with a no-op pid-controller (Kp=0, Ti=100.000 and Td=0). Input is your current pitch (/orientation/pitch-deg), reference is a target-pitch property (you wouldn't want a constant zero here, /autopilot/settings/target-pitch-deg) and output goes to elevator-cmd (/autopilot/internal/elevator-cmd).
Again increase you Kp slowly and check each step in FlightGear. I'd expect something like -0.025 resulting in an instanteanous full elevator deflection on a 40 degrees pitch offset. Do not try too large values; it results in instable behaviour very quickly. Now, decrease the value of Ti, probably down to 10 or so. The smaller it gets, the faster the elevator moves and the easier it is to get an unstable loop. Again it is very likely that you won't need Td to get a stable controller.
Simulating servos
Once you have this two axies running, you might want to think about simulating the slow movements of the servo or hydraulic motors that drive the control surfaces. You might have noticed, that the controls react rapidly when enabled which is unrealistic. In a Seneca, the aileron servo takes 12 seconds to move from left to right. The elevator needs 26 seconds and the pitch trim takes 45 seconds for full travel.
There is a filter to simulate that behaviour, it's the noise-spike filter. Here is a sample with 25 seconds transition time from -1 to +1:
<filter>
<name>SERVO-DRIVER:elevator</name>
<debug>false</debug>
<feedback-if-disabled>true</feedback-if-disabled>
<enable>
<prop>/autopilot/locks/altitude</prop>
<value>pitch-hold</value>
</enable>
<input>/autopilot/internal/elevator-cmd</input>
<output>/controls/flight/elevator</output>
<type>noise-spike</type>
<max-rate-of-change>0.08</max-rate-of-change>
</filter>
The formula for max-rate-of-change is (max-out - min-out)/transition-time. In our example (1--1)/25=2/25=0.08. Try it out, disable your pitch-hold controller and enable the servo-driver. Set your some/internal/elevator-cmd property to +1 and see your elevator moving to fully up or down within 12.5 seconds. Now set your property to -1 and it will take 25 seconds for the elevator to move to the other direction. The feedback-if-disabled property is needed, if this stage is driven from a pid-controller.
We said before that PID-controllers compute offsets to the curren value of it's output. If you disable the servo-driver from your A/P master switch, the value of your elevator is fed back to the internal value of the elevator-cmd so the pid-controller has a reasonable value to start from when it is enabled. Create servo drivers for elevator, aileron and the trim channels if you intend to use them. A good location for the internal driver properties is /autopilot/internal - but that is up to you.Now change your pitch and roll PID-controller so that they no longer directly drive the output properties but the internal properties. You might need to adjust the Kp and Ti values of the PID-controller because you introduced some lag with the driver. But you should end up with perfectly smooth operation of the two main channels.
Spend some time optimizing these pitch and roll channels, they will be used by all other modes and have to be stable in any case!
A simple auto throttle
<pid-controller>
<name>Auto throttle</name>
<debug>false</debug>
<enable>
<prop>/autopilot/locks/speed</prop>
<value>speed-with-throttle</value>
</enable>
<input>
<prop>/instrumentation/airspeed-indicator/indicated-speed-kt</prop>
</input>
<reference>
<prop>/autopilot/settings/target-speed-kt</prop>
</reference>
<output>
<prop>/autopilot/internal/throttle-cmd</prop>
</output>
<config>
<Kp>0.15</Kp>
<Ti>20.0</Ti>
<Td>0.00001</Td>
<u_min>0.0</u_min>
<u_max>1.0</u_max>
</config>
</pid-controller>
And a filter, as our autopilot does not move the throttle levers up/down in split seconds (you can imagine how that would be potentially harmful to the flight crew). Depending on the number of engines of your aircraft, you might want to add more or delete some output properties.
<filter>
<name>SERVO-DRIVER:throttle</name>
<debug>false</debug>
<feedback-if-disabled>true</feedback-if-disabled>
<enable>
<prop>/autopilot/locks/speed</prop>
<value>speed-with-throttle</value>
</enable>
<input>/autopilot/internal/throttle-cmd</input>
<output>
<prop>/controls/engines/engine[0]/throttle</prop>
<prop>/controls/engines/engine[1]/throttle</prop>
<prop>/controls/engines/engine[2]/throttle</prop>
<prop>/controls/engines/engine[3]/throttle</prop>
</output>
<type>noise-spike</type>
<max-rate-of-change>0.1</max-rate-of-change>
</filter>
Vertical speed hold
Let's start with some thinking. For vertical-speed-hold, the controller has to compare your current rate-of-climb with your reference rate-of-climb. The ROC will be controlled by setting the pitch of the aircraft. We already have a pitch-hold controller, so we will set the target-pitch from our rate-of-climb controller. To not make our passengers barf when we enable our controller, it's important to set the target-pitch relative to our current pitch - which once again calls for a pid-controller (because of it's relative output).
To get the current pitch to a property which will be modified by a pid-controller later, we will now create a controller which serves as a so called "sample-and-hold" element. It simply copies the input property value to the output property value until it is disabled. The same property, that disables the sample-and-hold controller will enable the pid-controller that computes the pitch for the rate-of climb.Here is a sample-and-hold controller implemented with a gain-filter:
<filter>
<name>AP:Pitch sample and hold</name>
<debug>false</debug>
<enable>
<condition>
<not>
<property>/autopilot/locks/roc-lock</property>
</not>
</condition>
</enable>
<type>gain</type>
<gain>1.0</gain>
<input>/orientation/pitch-deg</input>
<output>/autopilot/internal/target-pitch-deg</output>
</filter>
You probably have to adjust the property names. Note the condition element in the <enable> section - it has the same syntax as in the well-known <animation> elements. As long as the roc-lock property is false, input is copied to output. When it becomes true, the output property is no longer written by this filter and the next step can use this value as a start.
Right behind this filter, add a pid-controller using your current rate-of-climb as input and a target-rate-of-climb as reference. Enable this pid if your pitch sample-and-hold is not enabled (use the same condition, just remove the <not> elements). Your output goes to target-pitch-deg which should be the same property as the input of your pitch-hold controller. Use the same procedure to obtain the values for Kp and Ti. Kp will be small. The offset computes in feet per minute and 100fpm should result in a few degrees pitch change. Something like 0.01 or smaller will probably do. I'd expect Integrator time around 10-50. You should clamp the target-pitch to something less than +/- 90° - probably -10° and +20° or whatever is a reasonable value for your aircraft. It could look like this:
<pid-controller>
<name>Vertical speed pitch hold</name>
<debug>false</debug>
<enable>
<condition>
<property>/autopilot/locks/roc-lock</property>
</condition>
</enable>
<input>
<prop>/autopilot/internal/vert-speed-fpm</prop>
</input>
<reference>/autopilot/settings/vertical-speed-fpm</reference>
<output>
<prop>/autopilot/settings/target-pitch-deg</prop>
</output>
<config>
<Kp>0.001</Kp>
<Ti>10.0</Ti>
<Td>0.0</Td>
<u_min>-10.0</u_min>
<u_max>10.0</u_max>
</config>
</pid-controller>
Logic-controlled properties
These two new elements should give you a rate-of-climb hold mode for your autopilot. If you are not yet confused which controllers should be enabled in what mode, you will be very soon as we will add even more elements. If you are in the need for some logic-controlled properties, the autopilot can do this, too - no need for nasal helpers! There is a <logic> element that does all the magic:
<logic>
<name>Electrical Power</name>
<input>
<greater-than>
<property>systems/electrical/outputs/autopilot</property>
<value type="double">10.0</value>
</greater-than>
</input>
<output>autopilot/has-power</output>
<output>autopilot/servicable</output>
</logic>
This creates two boolean properties: has-power and serviceable, and sets them to true if the electrical system provides more than 10.0 volts. You can build complex logic tables to drive your analog stages of the autopilot.
Altitude hold
Now, that you have a ROC-hold mode, you most certainly want an altitude-hold with preselect. This will be a simple linear amplifier without any time dependencies. Again, you have to compare your current altitude with a reference value, but this time there will be no integrator involved. The bigger your offset from the reference altitude is, the bigger your rate of climb/descent should be. Let's assume, 1000ft offset should result in 500fpm ROC. The maximum rate of descent should be 1000fpm and the maximum rate of climb should be 2000fpm. Since there is no integrator involved, we use a simple gain filter to compute the rate of climb:
<filter>
<name>Target Rate of Climb Computer (ALT HOLD)</name>
<debug>false</debug>
<enable>
<prop>/autopilot/locks/altitude</prop>
<value>altitude-hold</value>
</enable>
<type>gain</type>
<input>position/altitude-ft</input>
<reference>autopilot/settings/target-altitude-ft</reference>
<gain>-0.5</gain> <!-- 1000ft offset gives 500fpm roc -->
<output>autopilot/internal/target-roc-fpm</output>
<min>-1000</min>
<max>2000</max>
</filter>
If everything goes well, your chain of controllers for altitude hold looks something like this:
- Target Rate Of Climb Computer target-rate-of-climb = (reference-altitude - current-altitude) * 5
- Target Pitch Computer computes target-pitch-deg as a function of current-roc and target-roc
- Elevator Command Computer computes elevator-deflection as a function of current pitch and target pitch
- Elevator Servo Driver makes smooth movements of the elevator
This is nearly all for altitude-hold with altitude-preselect. You can add an altitude-sample-and-hold stage to add a "hold my current altitude" function.
Heading hold
For heading hold, you build a two stage system. The first is a comparator comparing your selected heading and the indicated heading. 1. Stage, compute heading offset. Note the period element at the bottom, it normalizes the output into the periodic -180 to +180 range
<filter>
<name>Heading Offset Computer</name>
<debug>false</debug>
<type>gain</type>
<gain>1.0</gain>
<input>/autopilot/settings/heading-bug-deg</input>
<reference>/orientation/heading-magnetic-deg</reference>
<output>/autopilot/internal/heading-offset-deg</output>
<period>
<min>-180</min>
<max>180</max>
</period>
</filter>
The second stage computes a bank angle from a heading offset. At least for smaller planes, a rule of thumb says "roll out at half the bank angle". If you fly a right hand turn at a bank angle of, say, 20° and want to end at a heading of 270°, you start your roll out at 20°/2 = 10° before your target heading. So you start rolling out at 280°. We turn this rule around and say our bank angle is twice the heading error but not more that 30° This results in a simple gain filter. You might want to make the target roll computer switchable with an enable element, otherwise it will overwrite your target-roll-deg at all times.
<filter>
<name>Target Roll Computer</name>
<debug>false</debug>
<enable>
<prop>/autopilot/locks/heading</prop>
<value>dg-heading-hold</value>
</enable>
<type>gain</type>
<input>/autopilot/internal/heading-offset-deg</input>
<output>/autopilot/internal/target-roll-deg</output>
<gain>2.5</gain>
<min>-30.0</min>
<max>30.0</max>
</filter>
Most aircraft have pilot selectable bank-limits. You can implement these by editing the min and max options to something like this:
...
<min>
<property>/autopilot/settings/bank-limit</property>
<scale>-1.0</scale>
</min>
<max>/autopilot/settings/bank-limit</max>
...
This filter feeds your wing leveler which is already done, but it requires a little edit in the <enable> and <reference> parts in order to obtain a pilot-selectable roll:
...
<enable>
<prop>/autopilot/locks/heading</prop>
<value>dg-heading-hold</value>
</condition>
</enable>
...
<reference>
<prop>/autopilot/internal/target-roll-deg</prop>
</reference>
...
You can also play with the min/max values to have a nice standard rate turn. The bank angle for the standard turn is a function of your true airspeed; you can build a controller calculating the correct bank angle and feed this value into min/max (which is a commen feature on airliners).
Decreasing the gain of 2.5 should help, if your roll-rate is too slow.
NAV modes
In lateral nav mode, the primary goal of the autopilot is to intercept and maintain a certain ground track. This could be a VOR radial, a LOC course or back course or a GPS track. For most autopilots these are basically the same and it does not know anything about the source of the signal. Everything it knows is your current heading, your desired course and a course offset. And these are the properties, we need for our NAV hold mode.
Now, how does the autopilot knows which direction to steer? Lets assume you want to intercept the radial 270 of some VOR. VOR radials "radiate" away from the station, so the 270 radial points away from the station with a heading of 270degrees (which is west).Lets further assume you are somewhere south-west of the VOR station - this is between radial 180 and 270 - and you fly on a heading of 250 (this will become important later).
Now you have the most important facts at hand you need for navigation:
- Q1: Where am I?
- A1: South-west of the VOR
- Q2: Where would I want to be?
- A2: On radial 270 of the VOR
As a rule of thumb, interception of radials should occour at angles of 30° to 45°. Intercepting at angles lower than 30° takes ages to get you there, intercepting at greater than 45° angles might result in a very steep turn to not overshoot the radial. We pick 45° because I like this value most.Our next question to answer is
- Q3: Which way should I fly to get where I want to be?
- A3: To intercept the course of 270° at a 45° angle, fly 270°+45°=315°
Now that we know which way we should fly we can easily answer
- Q4: Which direction do I have to turn?
- A4: You are currently heading 250°. You want to fly heading 315°, so turn right by 65°.
That's basically all the magic about navigation. Now let's implement this in the autopilot. One part is already done: Q4/A4 is your heading-hold stage, so it's probably best to move backwards from that stage.
First, add a new, switchable input element to your heading offset computer. Just change this:
<filter>
<name>Heading Offset Computer</name>
<debug>false</debug>
<type>gain</type>
<gain>1.0</gain>
<input>/autopilot/settings/heading-bug-deg</input>
<reference>/orientation/heading-magnetic-deg</reference>
<output>/autopilot/internal/heading-offset-deg</output>
<period>
<min>-180</min>
<max>180</max>
</period>
</filter>
to this (that is, adding the upper input part):
<filter>
<name>Heading Offset Computer</name>
<debug>false</debug>
<type>gain</type>
<gain>1.0</gain>
<input>
<condition>
<equals>
<property>/autopilot/locks/heading</property>
<value>nav1-hold</value>
</equals>
</condition>
<property>/autopilot/internal/intercept-heading-deg</property>
</input>
<input>/autopilot/settings/heading-bug-deg</input>
<reference>/orientation/heading-magnetic-deg</reference>
<output>/autopilot/internal/heading-offset-deg</output>
<period>
<min>-180</min>
<max>180</max>
</period>
</filter>
Now, if /autopilot/locks/heading equals "nav1-hold", the heading offset computer uses the intercept-heading-deg property as the target heading to fly.For any other value, the old heading-bug value is used. You can already test, if it works. Use the property browser to set this stage to nav-hold and enter any arbitrary value into intercept-heading-deg. Your aircraft should turn to the entered heading on the shortest path. It is crucical that this mode is stable, so spend some time again and double check.
Next, we need to compute the intercept-heading-deg automatically. This is based on the offset from the desired course with a maximum intercept angle of 45°. The CDI is our friend, it tells us about our course offset, so we use that property in our stage feeding the Heading Offset Computer.
This is a very simple stage, it just substracts a course error (which we will compute in the next stage) from our selected course and normalizes the output into the [0..360] degree interval:
<filter>
<name>Intercept Heading Computer</name>
<debug>false</debug>
<type>gain</type>
<gain>1.0</gain>
<input>/autopilot/internal/selected-course</input>
<reference>/autopilot/internal/course-error-deg</reference>
<output>/autopilot/internal/intercept-heading-deg</output>
<period>
<min>0</min>
<max>360</max>
</period>
</filter>
This stage only makes sense with a preceding one computing the course error from the cdi deflection. We want something that spits out 45 for a full CDI deflection and one might think of a <gain> filter to do this. But we also want an automatic wind correction in case of cross wind. For that we also need an integrator. Because we need to compute absolute values for the output, a pi-simple-controller is our choice here:
<pi-simple-controller>
<name>cdi-integrator</name>
<debug>false</debug>
<config>
<Kp>45.0</Kp>
<Ki>0.40</Ki>
</config>
<input>/autopilot/internal/cdi-deflection</input>
<output>/autopilot/internal/course-error-deg</output>
<min>-45.0</min>
<max>45.0</max>
</pi-simple-controller>
It takes our CDI deflection and compares to zero (note the missing reference element which defaults to zero). The result is multiplied by Kp (45) and a value of 0.4° per second is added to the offset. The output is clipped to 45°.
You now have all stages, you need for the LNAV section. To compute the values in the correct order, they should be in the order
- cdi-integrator
- intercept heading computer
- heading offset computer
Related content
The FlightGear forum has a subforum related to: Autopilot |