Howto:Implement a Fly-By-Wire System for Airliners

From FlightGear wiki
Jump to navigation Jump to search
Note  This page is out of date, and does not demonstrate the best way to construct a stable fly-by-wire system.
This article is a stub. You can help the wiki by expanding it.

Objective: Implement a Nasal and PID Controller based fly-by-wire system for Airliners with fly-by-wire and set configuration properties to get desired results.

Background

The Fly-By-Wire system was developed for the Boeing 787-8, but can be used for other aircraft too. As Boeing wanted to give pilots the upper hand, the fly-by-wire systems CAN be disabled and overridden by pilot inputs. If you would like to use this fly-by-wire script for an Airbus Aircraft, you might want to make a few changes to give the fly-by-wire more power.

The whole idea here is to get the pilot's control inputs through a nasal script, and creating custom output properties which are then read by the FDM. To implement this system, you'll need to work with the following files:

  • -set.xml
  • fbw.nas
  • fly-by-wire.xml
  • FDM xml

Note that as the new 787 uses JSBSim and the FBW has only been tested on JSBSim, but you can also use this on YASim FDMs.

B787-8 Fly-By-Wire

Another feature in the Boeing 787-8's Fly By Wire is the automatic control of the trailing edge camber which affects lift and drag. The fly-by-wire automatically adjusts that to get maximum efficiency. Redneck and I am working on that but it's not included in the below nasal code as I don't think any other aircraft has that. And ofcourse, it's not yet completed. The rest of the Fly-By-Wire is stable at the moment and has been tested on the new Boeing 787-8.

Note for Airbus Aircraft Developers

About Airbus Fly By Wire : [1]

I've just seen that the airbus Fly By Wire works slightly different. I'll create a new fly-by-wire / modify this one to have it function that way. In Airbus Aircraft, moving the control stick controls angular acceleration, and not angular velocity. That is, when you move the stick to the left, the plane starts turning left, when you bring the stick back to the center, the fly-by-wire asks the plane to maintain that bank angle. It's the same case for pitch, and this is not available in Boeing's Fly-By-Wire.

Additional information on Airbus FBW systems can be found in this PDF (section 5.9/p.57+).

FBW differences between Airbus and Boeing aircraft

There's an interesting discussion available here [2].

The concept of FBW laws is explained here [3].

Functions

The Boeing 787-8 Fly-By-Wire system executes the following functions which help stabilize the aircraft:

  • Accordingly adjusts thrust and elevators to maintain lift during turns
  • Helps during turns with rudder movement
  • Doesn't allow the plane to turn beyond the specified bank limit (customizable)
  • Applies thrust and slightly moves the elevators down when closing in on stall speed
  • Reduces thrust when exceeding 250 knots under 10000 ft MSL and when reaching Vne above 10000 ft MSL
  • Dampens the controls to prevent sudden movements
  • Automatically holds the current aircraft pitch and roll degree when stick is brought back to the center! < NEW!
  • ALPHA PROTECTION doesn't allow the plane to pitch up higher than 20 degrees < NEW!


The Fly-By-Wire Configs can be set/edit from the property tree. In the Boeing 787-8 (new one coming up soon), you can edit the following FBW Configurations from the FBW CONFIG menu in the CDU:

  • Fly-By-Wire Status (ACTIVE / DISABLED)
  • Rudder Control (ALLOWED / DENIED)
  • YAW DAMPER (ACTIVE / DISABLED)
  • BANK LIMIT (CYCLE BETWEEN 15, 20, 25, 30, 35, 40)

Implementation

/Nasal/fbw.nas

Open your aircraft's nasal directory and create a file called 'fbw.nas'. Copy and paste the following code in that file and save it.

#########################################
## FLY BY WIRE SYSTEM FOR BOEING 787-8 ##
#########################################
## Designed by Omega, Hooray & Redneck ##
#########################################

# CONSTANTS

var RAD2DEG = 57.2957795;
var DEG2RAD = 0.0174532925;
var INCREMENT = 0.001;

# helpers:

##
# a wrapper to determine if a value is within a certain range
# usage:in_range(1,[min,max] );
# e.g.: in_range(1, [-1,+1] );
#
var in_range = func(value, range) {
 var min=range[0];
 var max=range[1];
 return ((value <= min) and (value >= max));
}

var fbw = {
	init : func { 
        me.UPDATE_INTERVAL = INCREMENT; 
        me.loopid = 0; 
		me.throttle = 0;
		me.throttlefix = 0;
		me.throttleinit = 0;
		me.targetthrottle = 0;
		me.turnthrottlefix = 0;
		me.targetaileron = 0;
		me.targetelevator = 0;
		me.targetrudder = 0;
		me.adjustelevators = 0;
		me.stabilize = 0;

		me.stabpitch = 0;
		me.stabroll = 0;

		me.disconnectannounce = 0;
		# use a vector of throttles, this can be later on used to support more than
		# just two engines
		me.throttles = [nil,nil]; 


## Initialize with FBW Activated

setprop("/controls/fbw/active", 1);
setprop("/controls/fbw/rudder", 1);
setprop("/controls/fbw/yaw-damper", 1);
setprop("/controls/fbw/bank-limit", 35);

## Initialize Control Surfaces

setprop("/fdm/jsbsim/fcs/aileron-fbw-output", 0);
setprop("/fdm/jsbsim/fcs/rudder-fbw-output", 0);
setprop("/fdm/jsbsim/fcs/elevator-fbw-output", 0);

        me.reset(); 
}, 
	update : func {

var fcs = "/fdm/jsbsim/fcs/";

## Fix Damp Rate according to Framerate

me.fpsfix = 1;
if (getprop("/sim/frame-rate") != nil) me.fpsfix = 25 / getprop("/sim/frame-rate");

## Bank Limit Setting

me.banklimit = getprop("/controls/fbw/bank-limit");

## Position and Orientation

me.altitudeagl = getprop("/position/altitude-agl-ft");

var altitudemsl = getprop("/position/altitude-ft");

var pitch = getprop("/orientation/pitch-deg");
me.roll = getprop("/orientation/roll-deg");

var airspeedkt = getprop("/velocities/airspeed-kt");

## Flight Control System Properties

var elevtrim = getprop("/controls/flight/elevator-trim");
var ailtrim = getprop("/controls/flight/aileron-trim");

me.aileronin = getprop(fcs~"aileron-cmd-norm");
me.elevatorin =  getprop(fcs~"elevator-cmd-norm");
me.rudderin = getprop(fcs~"rudder-cmd-norm");

## FBW Output (actual surface positions)

me.aileronout = getprop(fcs~"aileron-fbw-output");
me.elevatorout =  getprop(fcs~"elevator-fbw-output");
me.rudderout = getprop(fcs~"rudder-fbw-output");

## Engine Throttle Positions

var engines = props.globals.getNode("controls/engines").getChildren("engine");
forindex(var index; engines ) {
me.throttles[index] = engines.getNode("engine",index).getNode("throttle").getValue() );
}

me.throttles[0] = getprop("[0]/throttle");
me.throttles[1] = getprop("controls/engines/engine[1]/throttle");


## This is where the FBW actually does its job ;)

me.check_if_active();

if (getprop("/controls/fbw/active")) {

me.disconnectannounce = 0;

me.update_ailerons();
me.update_elevator();

## ALPHA PROTECTION

if (pitch > 20) {
setprop("/controls/fbw/alpha-protect", 1);
setprop("/controls/fbw/alpha-limit", 22);
} elsif (pitch < -20) {
setprop("/controls/fbw/alpha-protect", 1);
setprop("/controls/fbw/alpha-limit", -22);
} else setprop("/controls/fbw/alpha-protect", 0);

## PROTECTION END TRIM FIX

if ((getprop("/controls/fbw/alpha-limit") == 0) and (getprop("/controls/fbw/autostable") == 0) and (getprop("/autopilot/locks/altitude") == "")) {
if (getprop("/controls/flight/elevator-trim") < 0) setprop("/controls/flight/elevator-trim", getprop("/controls/flight/elevator-trim") + 0.03);
if (getprop("/controls/flight/elevator-trim") > 0) setprop("/controls/flight/elevator-trim", getprop("/controls/flight/elevator-trim") - 0.03);
}

## AUTO-STABILIZATION

### Get the aircraft to maintain pitch and roll when stick is at the center

if ( in_range(me.elevatorin, [-0.1,0.1]) and in_range(me.aileronin, [-0.1,0.1]) ) {

if (me.stabilize == 0) {
setprop("/controls/fbw/stabpitch-deg", pitch);
setprop("/controls/fbw/stabroll-deg", me.roll);
me.stabilize = 1;
}

if ((airspeedkt >= 220) and (me.altitudeagl >= 3500)) {
setprop("/controls/fbw/autostable", 1);
} else {
setprop("/controls/fbw/autostable", 0);
}

} else {
me.stabilize = 0;
setprop("/controls/fbw/autostable", 0);
}
## THROTTLE CONTROLS

### Disconnect Throttle fix if manually overridden

if (me.throttles[0] != me.throttle) {
me.throttlefix = 0;
me.turnthrottlefix = 0;
}


### Adjust throttle while turning

if ((me.roll <= -5) or (me.roll >= 5)) {

if (me.turnthrottlefix == 0) {
me.throttleinit = me.throttles[0];
me.turnthrottlefix = 1;
}

me.targetthrottle = me.throttleinit + (me.throttleinit * math.sin(math.abs(me.roll * DEG2RAD)))/2;

if (me.targetthrottle > me.throttles[0]) {
me.inc_throttles();
} elsif (me.targetthrottle < me.throttles[0]) {
me.dec_throttles();} 

}

if ( in_range(me.roll,[-5,5]) and (me.turnthrottlefix == 1) ) {


if (me.throttles[0] <= me.throttleinit - 0.05) {
me.inc_throttles();
} elsif (me.throttles[0] > me.throttleinit + 0.05) {
me.dec_throttles();
} else me.turnthrottlefix = 0;
}

### Reduce throttle if aircraft is faster than 250 KIAS under 10000 ft

if ((airspeedkt >= 250) and (altitudemsl <= 10000) and me.throttles_not_idle() ) {
me.dec_throttles();
me.throttlefix = 1;
}

if ((me.throttlefix == 1) and (airspeedkt < 245) and (altitudemsl <= 10000) and me.throttles_not_maxed() ) {
me.inc_throttles();
}

### Adjust Throttle to stay under Vne

if ((airspeedkt >= 350) and (altitudemsl > 10000) and me.throttles_not_idle() ) {
me.dec_throttles();
me.throttlefix = 1;
}

if ((me.throttlefix == 1) and (airspeedkt < 340) and (altitudemsl > 10000) and me.throttles_not_maxed() ) {
me.inc_throttles();
}

### Adjust Throttle to keep from stalling

if ((airspeedkt < 125) and (me.altitudeagl > 250) and me.throttles_not_maxed() ) {
me.inc_throttles();

### Also help by pushing forward on the stick

me.elevatorout += 0.02;

}

## RUDDER CONTROLS

if (getprop("/controls/fbw/rudder")) {

if ((me.roll < -5) or (me.roll > 5)) {
me.targetrudder = me.aileronout / 2;

if (me.targetrudder < me.rudderout) me.rudderout -= 0.015;
if (me.targetrudder > me.rudderout) me.rudderout += 0.015;

} }

me.update_yaw_damper();

# Transmit output signals to surfaces

setprop(fcs~"aileron-fbw-output", me.aileronout);
setprop(fcs~"elevator-fbw-output", me.elevatorout);
setprop(fcs~"rudder-fbw-output", me.rudderout);

setprop("controls/engines/engine[0]/throttle", me.throttles[0]);
setprop("controls/engines/engine[1]/throttle", me.throttles[1]);

me.throttle = me.throttles[0]; # This is to find out if the pilot moved the throttle

} else {

# Transmit input signals directly to surfaces

setprop(fcs~"aileron-fbw-output", me.aileronin);
setprop(fcs~"elevator-fbw-output", me.elevatorin);
setprop(fcs~"rudder-fbw-output", me.rudderin);

}

},
    update_elevator: func {
    ## ELEVATOR CONTROLS
    if (me.elevatorin > me.elevatorout) me.elevatorout += 0.05 * me.fpsfix;
    if (me.elevatorin < me.elevatorout) me.elevatorout -= 0.05 * me.fpsfix;
    if ((me.elevatorin - me.elevatorout < 0.05) and (me.elevatorin - me.elevatorout > 0)) me.elevatorout += 0.01; 
    if ((me.elevatorout - me.elevatorin < 0.05) and (me.elevatorin - me.elevatorout < 0)) me.elevatorout -= 0.01; 
},

    update_yaw_damper: func {
    ## YAW DAMPER

    if (getprop("/controls/fbw/yaw-damper")) {

    if (me.rudderin > me.rudderout) me.rudderout += 0.05 * me.fpsfix;

    if (me.rudderin < me.rudderout) me.rudderout -= 0.05 * me.fpsfix;

    } else {

    me.rudderout = me.rudderin;

    }

},
    update_ailerons: func {
      ## AILERON CONTROLS

      ### Set Aileron Direction and Roll Direction

      me.rolldir = 0;
      if (me.roll < 0) me.rolldir = -1;
      if (me.roll > 0) me.rolldir = 1;


      me.ailerondir = 0;
      if (me.aileronin < 0) me.ailerondir = -1;
      if (me.aileronin > 0) me.ailerondir = 1;


      if ( in_range(me.roll,[-me.banklimit,me.banklimit]) or (me.rolldir != me.ailerondir)) {


      if (me.aileronin > me.aileronout) me.aileronout += 0.05 * me.fpsfix;

      if (me.aileronin < me.aileronout) me.aileronout -= 0.05 * me.fpsfix;

      } else {

      ### Don't let the plane bank past the bank limit

      if (me.roll < -me.banklimit) me.targetaileron = -(me.roll + me.banklimit) * 0.025;
      if (me.roll > me.banklimit) me.targetaileron = -(me.roll - me.banklimit) * 0.025;

      if (me.aileronout < me.targetaileron) me.aileronout += 0.025 * me.fpsfix;
      if (me.aileronout > me.targetaileron) me.aileronout -= 0.025 * me.fpsfix;

      }

},
    check_if_active : func {
	### The Fly-by--wire only works when it is active. In the Boeing 787, pilots have the option to disable fly-by-wire and use power-by-wire* in case of emergencies. The Fly By Wire Configuration includes: On/Off, Bank Limit and Rudder Control. The FBW Configs can be set in the FBW CONFIG Page in the CDU(s)

	## Turn on Fly By Wire only if we have power

	if (getprop("/systems/electrical/outputs/efis") != nil) {
	  if ((getprop("/systems/electrical/outputs/efis") < 9) and (me.altitudeagl >= 200)) {
	  setprop("/controls/fbw/active", 0);
	  if (me.disconnectannounce == 0) {
	    screen.log.write("Fly By Wire Disconnected!", 1, 0, 0);
	    me.disconnectannounce = 1;
	  }
	}
	}
},

	inc_throttles: func {
	forindex(var t; me.throttles)
	  me.throttles[t] += INCREMENT * me.fpsfix;
},

	dec_throttles:func {
	forindex(var t; me.throttles)
	  me.throttles[t] -= INCREMENT * me.fpsfix;
},
	throttles_not_idle: func {
	  foreach(var t; me.throttles) {
	      if (me.throttles[t] == 0) return 0; # at least one throttle is idle
	    }
	return 1; # throttles are idle
},
	throttles_not_maxed: func {
	  foreach(var t; me.throttles) {
	      if (me.throttles[t] == 1) return 0; # at least one throttle is maxed
	    }
	return 1; # throttles are not maxed
},
    reset : func {
        me.loopid += 1;
        me._loop_(me.loopid);
    },
    _loop_ : func(id) {
        id == me.loopid or return;
        me.update();
        settimer(func { me._loop_(id); }, me.UPDATE_INTERVAL);
    }

};

fbw.init();
print("Fly-By-Wire ......... Initialized");

# *Power-by-wire : corresponds to power steering in cars

fly-by-wire.xml

Open your aircraft's Systems directory and create a file called 'fly-by-wire.xml'. Copy and paste the following pid controller code in that file and save it.

<?xml version="1.0"?>



<!-- Fly By Wire, Auto Stabilize Pitch and Roll -->



<PropertyList>



<!-- =============================================================== -->

<!-- Roll Deg Hold -->

<!-- =============================================================== -->


	<pid-controller>
		<name>FBW Roll Deg Hold</name>
		<debug type="bool">false</debug>
		<enable>
			<prop>/controls/fbw/autostable</prop>
		</enable>
		<input>
			<prop>/orientation/roll-deg</prop>
		</input>
		<reference>
			<prop>/controls/fbw/stabroll-deg</prop>
		</reference>
		<output>
			<prop>/controls/flight/aileron-trim</prop>
		</output>
		<config>
			<Kp>0.05</Kp>
			<beta>1</beta>
			<alpha>0.1</alpha>
			<gamma>0</gamma>
			<Ti>10</Ti>
			<Td>0.00001</Td>
			<u_min>-2</u_min>
			<u_max>2</u_max>
		</config>
	</pid-controller>


<!-- =============================================================== -->

<!-- Pitch Hold -->

<!-- =============================================================== -->

	 <pid-controller>
	    <name>Pitch hold</name>
	    <debug>false</debug>
	    <enable>
			<prop>/controls/fbw/autostable</prop>
	    </enable>
	    <input>
 	     <prop>/orientation/pitch-deg</prop>
 	   </input>
 	   <reference>
 	     <prop>/controls/fbw/stabpitch-deg</prop>
 	   </reference>
 	   <output>
 	     <prop>/controls/flight/elevator-trim</prop>
 	   </output>
 	   <config>
 	     <Kp>-0.05</Kp>      <!-- proportional gain -->
 	     <beta>1.0</beta>    <!-- input value weighing factor -->
 	     <alpha>0.1</alpha>  <!-- low pass filter weighing factor -->
 	     <gamma>0.0</gamma>  <!-- input value weighing factor for -->
 	                         <!-- unfiltered derivative error -->
 	     <Ti>1.0</Ti>        <!-- integrator time -->
 	     <Td>0.00001</Td>    <!-- derivator time -->
 	     <u_min>-1.0</u_min> <!-- minimum output clamp -->
 	     <u_max>1.0</u_max>  <!-- maximum output clamp -->
 	   </config>
 	  </pid-controller>

<!-- =============================================================== -->

<!-- Alpha Protection -->

<!-- =============================================================== -->

	 <pid-controller>

	    <name>Alpha Protection</name>

	    <debug>false</debug>

	    <enable>

			<prop>/controls/fbw/alpha-protect</prop>

	    </enable>

	    <input>

 	     <prop>/orientation/pitch-deg</prop>

 	   </input>

 	   <reference>

 	     <prop>/controls/fbw/alpha-limit</prop>

 	   </reference>

 	   <output>

 	     <prop>/controls/flight/elevator-trim</prop>

 	   </output>

 	   <config>

 	     <Kp>-0.05</Kp>      <!-- proportional gain -->

 	     <beta>1.0</beta>    <!-- input value weighing factor -->

 	     <alpha>0.1</alpha>  <!-- low pass filter weighing factor -->

 	     <gamma>0.0</gamma>  <!-- input value weighing factor for -->

 	                         <!-- unfiltered derivative error -->

 	     <Ti>1.0</Ti>        <!-- integrator time -->

 	     <Td>0.00001</Td>    <!-- derivator time -->

 	     <u_min>-1.0</u_min> <!-- minimum output clamp -->

 	     <u_max>1.0</u_max>  <!-- maximum output clamp -->

 	   </config>

 	  </pid-controller>



</PropertyList>

Note that the above PID Controller is configured for smooth control of the 787-8. You migh thave to tweak the values inside the <config> .. </config> tags to get to smooth with your aircraft.

-set.xml

Call the Nasal file in your aircraft's -set.xml file by adding the following code inside <nasal></nasal>

	<fbw>
		<file>Aircraft/787-8/Nasal/fbw.nas</file>
	</fbw>

And also add the following code under </sim>

	<systems>
		<autopilot>
			<path>Aircraft/787-8/Systems/fly-by-wire.xml</path>
		</autopilot>
	</systems>

Replace '787-8' with your aircraft folder name in both instances.

JSBSim FDM

Replace 'aileron-cmd-norm' with 'aileron-fbw-output', replace 'elevator-cmd-norm' with 'elevator-fbw-output' and finally, replace 'rudder-cmd-norm' with 'rudder-fbw-output'.

For example,

   <summer name="Pitch Trim Sum">
      <input>fcs/elevator-cmd-norm</input>
      <input>fcs/pitch-trim-cmd-norm</input>
      <clipto>
        <min> -1 </min>
        <max>  1 </max>
      </clipto>
   </summer>

was changed to

   <summer name="Pitch Trim Sum">
      <input>fcs/elevator-fbw-output</input>
      <input>fcs/pitch-trim-cmd-norm</input>
      <clipto>
        <min> -1 </min>
        <max>  1 </max>
      </clipto>
   </summer>

YASim FDM

Replace '/controls/flight/aileron' with '/fdm/jsbsim/fcs/aileron-fbw-output', replace '/controls/flight/elevator' with '/fdm/jsbsim/fcs/elevator-fbw-output' and finally, replace '/controls/flight/rudder' with '/fdm/jsbsim/fcs/rudder-fbw-output'.

For Example, change

	<control-input axis="/controls/flight/aileron" control="FLAP1" split="true"/>

with this

	<control-input axis="/fdm/jsbsim/fcs/aileron-fbw-output" control="FLAP1" split="true"/>

You may be wondering why a /fdm/jsbsim/fcs property is called in a YASim FDM, so here's the answer. That is JUST THE PROPERTY, it has nothing to do with the FDM. As the FBW was built for a JSBSim aircraft, the properties were simply placed there.

Configuration

Add the following switches anywhere (preferably the CDU) to be able to configure the CDU in FlightGear:

  • /controls/fbw/active (controls whether the fly-by-wire system is active or not)
  • /controls/fbw/yaw-damper (controls the yaw damper)
  • /controls/fbw/rudder (controls whether the fly-by-wire controls rudder or not)
  • /controls/fbw/bank-limit (controls the aircraft's bank limit)

If you don't want any of the configurations to be edited in Flightgear, but configured generally, add the following under </sim> to your -set.xml for the appropriate properties you want to configure:

	<controls>
		<fbw>
			<yaw-damper type="int">1</yawdamper>
			<rudder type="int">1</rudder>
			<bank-limit type="int">30</bank-limit> <!-- 30 degrees bank permitted -->
		</fbw>
	</controls>

Related content