Global Navigation: Navigation Functions

A collection of navigation functions written in 'C' relating to the simu­lation of aircraft navigation equipments and their terrestrial environ­ments. The func­tions are available in airnav.c which is best viewed in a highlighting ed­itor such as gedit.


  1. The software data structure containing information about the aircraft which relates to its navigation.

  2. The software structure and simulation of the aircraft's terrestrial radio environment.

  3. Computing the signal strength of a given station at the aircraft. This includes:

    1. The mathematical simulation of the antenna radiation lobes of various types of navigational aid.

    2. Simulation of the Morse code call sign keying of the radio navigational aids which are currently being received by the aircraft's receivers.

    3. Simulation of the tuning characteristics of the aircraft's continuously tunable LF/MF ADF receivers.

    4. Types of aircraft navigation receivers plus software data structures and procedures to simulate their operation.

  4. Using LF/MF and VHF radio navigation stations as air route way points including the mathematics of course intercepts and the functions which simulate them.

  5. Simulation of the aircraft's linear and rotational position and motion and also its on-board attitude reference systems.

  6. Simulation of the runway landing task and the Instrument Landing System.

  7. Overview and integration of the simulation software.

Aircraft Data Structure

A collection of navigation functions written in 'C' relating to the simu­la­tion of aircraft navigation equipments and their terrestrial environ­ments.

Details of the aircraft's position is maintained by the flight program in a structure:

struct AirData {
    Lat,    // aircraft's latitude
    Lng,    // aircraft's longitude
    Ht,     // height above sea-level (Earth Radians)
    GndHt,  // estimated height of ground beneath aircraft
    Hdg,    // ground-track heading (compass + wind − drift angle)
    Des,    // aircraft's angle of descent 
    Spd,    // aircraft's true horizontal ground speed
    Pitch,  // aircraft's pitch angle
    Roll,   // aircraft's roll angle
    Rot,    // rate-of-turn in radians per second
    TD;     // diameter of aircraft's minimum turning circle
  int Sw;   // Id of NAV receiver switched to navigation computer
} Air;

The Radio Environment

Details of all radio navigation stations are entered into and kept in a Ground Station Data Source file GSS.DAT. A compact ground station data file GS.DAT is then generated from GSS.DAT by the Ground Station Data compiler.

Air navigation functions: preparation of the ground station database.

The record for each station within GS.DAT has the following structure:

struct StnData {
  char    Type;       // NDB,RRG,VOR,ILS,DME,AMK,BMK,OMK,MMK,IMK
  double  Lat,        // Latitude
          Lng,        // Longitude
          Ht,         // Ht of stn above sea level (Earth radians)
          Rng,        // Effective Range
          Hdg;        // Runway or airway heading
  int     RadPat;     // 0=omni,1=RRG,2=Marker lobe,3=ILS lobe
  long    Frq;        // Frequency
  char CallSign[16];  // call sign coding string
  time_t  TimeOut,    // Time before next dist & bearing check
          OldTime;    // Time when dist & bearing last checked
  BOOL Receivable;    // Flag saying if station is in range
} Stn;

The first 9 items are constants for a given station, the last 3 vary with aircraft posi­tion. GS.DAT is generated from a ground station data source file GSS.DAT into which details of each ground station are typed initially.

Active Stations List

For a station to be active during simulation, its details have to be in memory. So we need to copy the details of any station which is in (or about to come into) range of the aircraft into a data structure in memory. Conversely, to conserve memory, we must delete stations from memory when they go more than a certain amount out of range of the aircraft.

Air navigation functions: abstracting the active stations list.

So that we can refer to an active station's data easily, we make the active stations list comprise an array of pointers, each element of which contains a pointer which points to a station's data structure as follows:

Air navigation functions: data access structure for receivable stations.

Station data is kept in a structure of type TxData. Tx is the name of the array so the pointer-constant, Tx points to the first element of the array. NumTx is the maximum number of elements in the array, ie the maximum number of stations we can acco­mmodate concurrently. TX points to the last element + 1 of the array. TxHi points to the highest currently occupied element. tx points to the element containing the pointer to the data pertaining to the station which the program is currently dealing with.

The TxData Structure

The data structure for each currently-active station has the following contents.

  int StnNum,          // Stn's record No within GS.DAT file
      StnType;         // see below
  double Lat,          // Latitude
         Lng,          // Longitude
         Ht,           // Ht of stn > sea level in Earth radians
         Rng,          // Effective Range
         Hdg,          // Runway or airway heading
         Len,          // Runway length (for ILS only)
         GS;           // Glide Slope Angle (for ILS only)
  int  RadPat;         // 0=omni, 1=RRG, 2=Marker lobe, 3=ILS lobe
  long Frq;            // Frequency
  char CallSign[16];   // call sign coding string
  BOOL Single;         // morse version of callsign in Morse[]

  double Dst, PrevDst, // Current & previous dist from aircraft
         Brg,          // Current true bearing from aircraft
         Sig;          // Signal strength of tx at aircraft
  char *pCallSign,     // ptr to current posn in callsign string
       Morse[48],      // Morse sequence in timer durations
       *pMorse;        // points to current Morse element
  int Repeat,          // Nº of morse seq repeats still to do
      Pitch,           // audio freq of morse tone (see below)
      MorseTimer;      // duration of current Morse Code element
  BOOL MorseKey;       // current state of Morse Key 
*Tx[NumTx],            // ptrs to NumTx active stn structures
*(*TxHi);              // ptr to Tx[highest occupied element+1]

StnType   1 = NDB, 2 = RRG, 3 = VOR, 4 = VOR/DME, 
          5 = ILS, 6 = ILS/DME, 7 = DME, 8 = AMK, 
          9 = BMK, 10 = OMK, 11 = MMK, 12 = IMK

Pitch     0 = 1020Hz, 1 = 1300Hz, 2 = 3000Hz

The items above the blank line are fixed items copied from the GS.DAT file. The items below the blank line are updated by the real-time NAV software.


When a station comes within receivable range, this function copies its details into a newly-allocated TxData structure. Apart from simply copying the fixed data items from the appropriate GS.DAT disk record, SetUpTxData() also possibly sets up the station's callsign for keying. If a station has a simple callsign sequence (ie only one semi-colon appears in it - see later in the section on callsign keying), SetUpTxData() calls SetUpCallSign() which translates the callsign sequence into Morse code once for the whole time the station is active. If the station has a multiple callsign (ie more than one semi-colon appears in it) then each segment of the callsign is set up dyn­amically during the on-going callsign keying cycle.

void SetUpTxData(struct TxData *t) {
  char *u = Stn.CallSign,  // ptr to callsign data in disk stream
       *v = t->CallSign;   // ptr to callsign in TxData structure
  t->Type = Stn.Type;      // Type of Station
  t->Lat = Stn.Lat;        // Its Latitude
  t->Lng = Stn.Lng;        // Its Longitude
  t->Ht = Stn.Ht;          // Its height > sea level (Earth rads)
  t->Rng = Stn.Rng - RNG;  // True transmission range
  t->Hdg = Stn.Hdg;        // runway or airway heading
  t->RadPat = Stn.RadPat;  // antenna radiation pattern or gain
  t->Frq = Stn.Frq;        // Frequency
  if(*u++ = 'S')           // S = single, M = multiple callsign
    t->Single = TRUE;      // single callsign
  else t->Single = FALSE;  // multiple callsign
  for(;*u > '\0';u++,v++)  // copy all but S/M byte of callsign in
    *v = *u;               // to TxData callsign buffer CallSign[]
  *v = '\0';               // and terminate it with a null char
  t->pCallSign = Stn.CallSign;  // set ptr to start of callsign
  t->MorseKey = TRUE;      // for first Morse element
  t->pMorse = t->Morse;    // set ptr to start of Morse Code
  if(t->Single == TRUE)    // if it is a single-part callsign
    SetUpCallSign(t);      // translate it to Morse once-only here

Stations are added to and deleted from the active stations list by the function:

BOOL SetStn(int StnNum,   // Stn's Record Nº in GS.DAT file
            BOOL flag) {  // TRUE if new stn just come in range

  #define RNG 0.0157079632675  // 100km in Great Circle radians
  BOOL result = FALSE;
  static struct TxData 
    *(*TX) = Tx + NumTx,  // ptr to last element in ptr array
    *t,                   // ptr to subject Tx Data structure
    **tx;                 // ptr to ptr to Tx Data structure


    for(tx = Tx;tx < TX;tx++) {  // find spare slot in ptr array
      if(*tx == NULL) {          // then allocate memory for station data
        *tx = (struct TxData *)malloc(size_t sizeof(struct TxData));
        if(*tx != NULL)
          result = TRUE;
    if(result == TRUE) {  // if enough memory could be allocated
      if(TxHi < tx)       // keep track of the 
        TxHi = tx;        // highest used element
      SetUpTxData(*tx);}  // copy stn data to TxData structure

  } else {                // ELSE A STATION HAS JUST GONE OUT OF RANGE

    for (tx = Tx;tx < TX;tx++) {  // find its Tx Data
      if((*tx)->StnNum == StnNum) {
        free(*tx);        // free the memory used by this station
        *tx = NULL;       // clear ptr in TxData ptr array
        if(TxHi == tx)    // keep track of the
          TxHi--;         // highest used element
        result = TRUE;

Rough Distance Check

When the simulation program is first started up, part of the initialisation process must be to calculate accurately and fully the distances of all ground stations rela­tive to the aircraft's starting position. This must also be done whenever the aircraft is artificially repositioned by the flight simulation instructor.

From then on, however, the distance of each station from the aircraft must be re­calculated regularly to see if an out-of-range station has moved into range or an in-range station has moved out of range. Calculation of distance by spherical geo­metry is time-consuming. So we must find a method of checking stations which re­sorts to recalculating their distances fully only when it absolutely has to.

If the aircraft has a maximum speed VMAX and the last calculated distance to a given station is 'd' and the effective transmitting range of the station is Stn.Rng, then the aircraft cannot possibly get into range of the station within a time less than (d - Stn.Rng)/VMAX no matter what direction the aircraft is travelling in. This formula provides a quick means of checking when we need to re-compute the station's true distance from the aircraft. This principle may be visualised as follows. Whenever a station's true distance is recalculated, the station emits a ghost which travels towards the aircraft at a constant speed VMAX irrespective of the aircraft's own speed or heading:

Air navigation functions: calculating the distance recalculation interval for approaching stations.

It is not necessary to re-calculate the station's distance until a time Stn.TimeOut has elapsed. The ghost behaves a bit like a photon which will move towards an ob­server at the speed of light 'c' irrespective of the relative speed of the Earth and the observed star which emitted it. We therefore cycle through the GS.DAT file con­tin­ually decrementing each station's Stn.TimeOut each pass by the amount of time that has elapsed since the previous time we checked that station.

Great-Circle Distance

Air navigation functions: the great circle distance calculation.

When a station's Stn.TimeOut expires we call the function GetDst() to compute its true accurate great-circle distance as follows:

double GetDst() {               // COMPUTE THE DISTANCE OF AN
  #define Pi 3.1415926535       // OBSERVED FROM THE OBSERVER
    dlong = Stn.Lng - Air.Lng,  // Longitudinal difference
    SinAirLat = sin(Air.Lat),   // Sine of latitude of observer
    CosAirLat = cos(Air.Lat),   // Cosine latitude of observer
    SinStnLat = sin(Stn.Lat),   // Sine of latitude of observed
    CosStnLat = cos(Stn.Lat);   // Cosine latitude of observed
  if(dlong >  Pi) dlong -= Pi;  // change the range of dlong 
  if(dlong < -Pi) dlong += Pi;  // from 0 - 2p to -p - +p 
  CosDst = CosAirLat * CosStnLat * cos(dlong)
         + SinAirLat * SinStnLat;
  if(CosDst > +1) CosDst = +1;  // avoid acos() domain errors
  if(CosDst < -1) CosDst = -1;
  return(acos(CosDst));         // return distance in radians

If as a result, the station is now found to be in range of the aircraft, its details are copied into a TxData structure if it is not already in one. If as a result, the station is now out of range of the aircraft, its TxData structure is deleted if it exists.

NOTE: Simple spherical geometry is used in the above function. The Vin­centy iterative ellipsoidal methods for computing accurate distance and bearing had not been invented at the time the software described herein was originated.

Distance-Checking Cycle

The following 'C' function determines whether or not a ground station is currently receivable at the aircraft. It is called about every 200 milliseconds.

void StnPoll() {
  #define VMAX 0.4363323129861111E-4  // 1000km/hr in radians/sec
  static int StnNum = 0;     // Station No preserved from prev pass
  double d;                  // distance of station from aircraft
  if (StnNum == NumStn)      // Station's record number in the
    StnNum = 0;              // Ground Station Data file GS.DAT
  GetStn(++StnNum);          // get data of next stn in GS.DAT file
  if (Stn.TimeOut > 0) {     // if not yet time to recalculate distance

    /* Decrement the station's distance re-calculation timeout,
    and advance the station's ghost towards the aircraft. */
    Stn.TimeOut -= SysTime - Stn.OldTime; Stn.OldTime = SysTime;

  } else {  // Else station's ghost is now in range of aircraft, so...

    /* If aircraft is within station's signal range, and station 
    is not currently on the 'receivable stations' list then.. */

    if(((d = GetDst()) < Stn.Rng) && (Stn.Receivable == FALSE)) {

      /*Attempt to put the station on the 'receivable' list and
      if successful store its updated status to the GS.DAT file*/

      if((Stn.Receivable = SetStn(StnNum, TRUE)) == TRUE)

    } else {  // Else the station is out of receivable range, so...

      /* If station currently on 'receivable' list, delete it
      from 'active stations' list and set it as unreceivable. */

      if (Stn.Receivable == TRUE) {
        SetStn(StnNum, FALSE);
        Stn.Receivable = FALSE;
      /* Set the timeout to next distance check and then
      store the updated station data to the GS.DAT file. */
      Stn.TimeOut = (d - Stn.Rng) / VMAX; PutStn(StnNum);

Stn.Rng as given in GS.DAT is actually 100km more than the maximum range at which the station can be received. This gives StationPolling() time to get the station on to the active list since an aircraft doing 1000km/hr can get 100km closer to a station in less time it takes to check 2,000 stations at 200ms per station.

Signal Strength

The signal strength, as delivered by a radio-navigation station at a given aircraft position, depends on its transmitter power, its radiation pattern and line-of-sight criteria.

The signal strength, as delivered by a radio-navigation station at a given aircraft position, depends on:

Omni-directional stations such as non-directional beacons [NDBs] and VHF omni-directional range stations [VORs] radiate equal signal power in all horizontal direc­tions. Their radiation patterns are therefore shown as circles:

Air navigation functions: calculating the signal strength of a VOR station at the aircraft.

The following function computes the signal strength for any station (such as a VOR or an NDB) with an omni-directional radiation pattern, ie one which radiates with equal strength in all horizontal directions.

double OmniSignal( struct TxData *t ) {
  return((t->Rng - t->Dst) / t->Rng);

A radio range station [RRG] radiates in 4 broad horizontal lobes as follows:

Air navigation functions: calculating the signal strength and content for a four-lobe radio range station.

The following function uses the current bearing of the aircraft from the radio range station to determine whether the aircraft is in one of the station's 'A' quadrants or in one of its 'N' quadrants. It then places an 'A' or an 'N' respectively in the 4th char­acter position of the first segment of the station's callsign so that the function CallSignKeying() will key an 'A' or an 'N' as appropriate.

The function returns the directional attenuation factor of the station's signal com­puted according to a radio range station's 4-leaf clover radiation pattern given by the formula: Signal = MaxSignal * sin( 2 * Bearing ).

double RadioRange( struct TxData *t ) {
  #define Pi 3.1415926535
  #define HalfPi 1.5707963267
  char s;             // contains 'A' or 'N' according to quadrant
  double b = t->Brg;  // bearing of aircraft from station
  if(b > Pi)          // if aircraft in 3rd or 4th quadrants,
    b -= Pi;          // consider it in the first or second
  if(b > HalfPi)      // if aircraft in the second or 4th quadrants
    s = 'N';          // key the letter 'N'
  else                // if aircraft in the first or 3rd quadrants
    s = 'A';          // key the letter 'A'
  // Put letter in 4th character position of first callsign segment
  *( t->CallSign + 3 ) = s;  
  // Compute directional reduction in station's effective range
  double a = t->Rng * sin( b + b );
  // return the current signal strength of station at aircraft
  return( ( a - t->Dst ) / a );

Radio marker beacons are placed at strategic points along airways (airways mark­ers) and at approaches to airport runways (landing markers). Landing markers are only effective up to 8,000 feet. A Marker's horizontal radiation pattern is elliptical with the minor axis of the ellipse in line with the airway or the runway's extended centre-line as shown below for a typical runway approach.

Air navigation functions: calculating the signal strength and content for runway marker stations.

Markers operate on a single fixed radio frequency. Airways markers transmit a call sign in slow morse code. Landing markers transmit slow morse signals as shown above. To simulate a marker beacon we must first calculate its signal strength at the aircraft. The signal strength at the aircraft's marker receiver depends on the air­craft's height, distance and relative bearing from the marker, the marker's trans­mitter power and the radiation pattern of the marker's antenna. The power and radiation pattern of the marker are constants, so we can say that:

Signal strength, S = F(h, d, F)

In detail, the signal strength at the aircraft is calculated as follows:

Air navigation functions: detailed horizontal geometry for the radiation lobe of a runway marker station.

Having found a we can work with a side view of the marker's vertical radiation lobe as shown below rather than at the odd relative bearing angle. The lobe (which has an elliptical horizontal cross-section) is a surface over which the signal is a constant strength. An approximate formula for the vertical cross-section is:

Air navigation functions: detailed vertical geometry for the radiation lobe of a runway marker station.

The following QuickBASIC program generates the above lobe. It also produces two small side-lobes at the bottom which are rather true to life. Note that the formula used for this simulation bears no resemblance to the mechanism which produces real antenna radiation patterns.

screen 9                  'switch to EGA enhanced mode graphics
window(-50,-20)-(+50,+80) 'move window origin to lower centre
XYscaler = 4/5            'PC screen X scaling is 4/5 its Y
Pi = 3.1415926535 : A = Pi / 2
Rmax = 60                 'simulates signal strength
n = 3.5                   'simulates antenna gain
for theta = -A to +A step .0005
x = cos(3.5 * theta) * cos(theta)   'lobe profile formula
if x => 0 then
  R = Rmax * sqr(x)
  a = R * sin(theta)         'distance component along airway x
  h = R * cos(theta)         'height y
  pset(a * XYscaler, h), 15  'plot the point
end if
next : end

The following function computes the signal strength within the field of a high-gain antenna as used by a marker beacon or an ILS station. The square root expression is the distance 'R' along the central axis of the station's radiation lobe at which the signal is the same strength as it is at the aircraft.

double RadiationLobe (
  double rsq,    //square of distance between aircraft and station
  double R,      //maximum effective range of station
  double theta;  //relative bearing between aircraft and lobe axis
) {
  return((R - sqrt(rsq /(cos(3.5 * theta) * cos(theta))))/ R);

The function below finds a marker's signal strength at the aircraft. The range of a marker beacon is given as a vertical range or height. This is because a marker's radiation lobe points upwards rather than horizontally like a Radio Range or ILS. The function finds the signal strength at the aircraft by finding the height on the central vertical axis of the marker's radiation lobe at which the signal is the same strength as at the aircraft. It then compares this with the maximum range of the marker (vertically) to find the relative signal strength at the aircraft.

double MarkerBeacon( struct TxData *t ) {
  double         // relative bearing of aircraft from marker axis
    x = sin(t->Brg - t->Hdg),
    d = t->Dst,  // horizontal distance of aircraft from marker
    h = Air.Ht,  // aircraft height

    /* Compute the square of the distance along the airway at the height 
       of the aircraft at which marker's signal strength is the same as
       it is at the aircraft. */
    asq = (d * d) / (1 + 3 * x * x),

    /* Compute the angle from the vertical axis of the marker's radiation 
       lobe to the point along the airway at which the marker's signal
       strength is the same as it is at the aircraft. */
    theta = atan(sqrt(asq) / h );
  return(RadiationLobe(asq + h * h, t->Rng, theta));

An instrument landing system's localiser transmitter sends a narrow beam centred along the runway heading, t->Hdg. In fact, it actually transmits two signals forming two narrow lobes, one angled slightly to the left and the other angled slightly to the right of the runway centre line. These provide the aircraft receiver with signals that enable it to determine the aircraft's current horizontal deviation from the runway centre-line. However, for the purpose of computing signal strength for callsign key­ing the signal may be regarded as a single lobe centred along the runway heading:

Air navigation functions: detailed horizontal geometry for the radiation lobe of an ILS localiser.

The following function uses the above to compute the signal strength at the aircraft which is somewhere within range of an ILS localiser station.

double ILSlocaliser( struct TxData *t ) {
  double r = t->Dst;  // distance between aircraft and station
  return ( RadiationLobe( r * r, t->Rng, t->Brg - t->Hdg ) );

The function below uses spherical geometry to compute the distance between the aircraft and the station whose TxData is pointed to by 'q'. It then computes the bearing as follows:

BOOL DandB (
  struct TxData *t,  // pointer to relevant station's details
  double MaxDst,     // maximum relevant distance
  BOOL FromAircraft  // if TRUE swap observer and observed
) {
  #define Pi 3.1415926535
    dlong = t->Lng - Air.Lng,    // Longitudinal difference
    SinAirLat = sin(Air.Lat),    // Sine of latitude of observer
    CosAirLat = cos(Air.Lat),    // Cosine of latitude of observer
    SinStnLat = sin(t->Lat),     // Sine of latitude of observed
    CosStnLat = cos(t->Lat);     // Cosine of latitude of observed
  if(dlong >  Pi) dlong -= Pi;   // change dlong's range
  if(dlong < -Pi) dlong += Pi;   // from 0 - 2p to -p - +p 
  double CosDst = CosAirLat * CosStnLat * cos(dlong)
                + SinAirLat * SinStnLat;
  if(CosDst > +1) CosDst = +1;   // avoid possible acos()
  if(CosDst < -1) CosDst = -1;   // domain errors
  if((double Dst = acos(CosDst)) > MaxDst) return( FALSE );
  double Brg = 0;
  // avoid possibility of division by zero 
  if((double SinDst = sin(Dst)) > 0)
    if(FromAircraft == TRUE) {   // if doing bearing of the station
      double a = SinStnLat;      // from the aircraft, then swap 
      SinStnLat = SinAirLat;     // the aircraft and station 
      SinAirLat = a;             // co-ordinates over.
    double CosBrg = (SinStnLat - SinAirLat * CosDst)
                  / (CosAirLat * SinDst);
    if(CosBrg < +1 && CosBrg > -1) {  // avoid possible acos()
      Brg = acos(CosBrg);             // domain errors
      if(dlong < 0) Brg += Pi;
    else if(CosBrg < 1) Brg = Pi;
  t->Dst = Dst; t->Brg = Brg;   // put dist & brg in station's
  return(TRUE);                 // TxData array

VHF Line of Sight

A radio-navigation station which operates on VHF (t->Frq > 30MHz) cannot be re­ceived if it is beyond the aircraft's horizon (ie if the aircraft's height is less than two thirds of the square of its distance from the station. This line-of-sight limit does not apply if the aircraft is within 20 nautical miles (37 km) of the station:

Air navigation functions: VHF line of sight calculation.

A function to compute the signal strength a transmitter delivers at the aircraft's cur­rent position according to distance and line-of-sight criteria is show below:

double TxSig(struct TxData *t) {
  #define LOS .005811946408975  // 37km Line-Of-Sight 
                                // (expressed in Earth radians)
    (*pf[ ])(struct TxData *) =  // array of ptrs to functions
      OmniSignal,    // omni-directional radiation patterns
      RadioRange,    // 4-leaf clover radiation pattern
      MarkerBeacon,  // vertical lobe radiation pattern
      ILSlocaliser,  // horizontal lobe radiation pattern
    } Los,           // line-of-sight distance limit
      Sig;           // tx's signal strength at aircraft

  if(t->Frq > 30000) {  // If it is a VHF station (freq > 30MHz)

    /* Compute line-of-sight range limit. If less than 
       horizon scatter range (20 nautical miles or 37 km) 
       set line-of-sight range = scatter range. */

    if((Los = sqrt(1.5 * (Air.Ht - t->Ht))) < LOS) Los = LOS;	
  else Los = Pi;  // else set it to half Earth circumference

  /* If station is effectively within 'line-of-sight', compute 
     signal strength of station at aircraft. If signal strength 
     returned is negative, make it zero. */

  if((DandB(t, Los, FALSE) == TRUE) 
  && ((Sig = (*(pf + t->RadPat))(t)) < 0))
    Sig = 0;
  else Sig = 0;  // zero signal strength if beyond line-of-sight
  return(Sig);   // return signal strength at aircraft

Callsign Keying

The various kinds of radio navigation stations key their callsigns in differ­ent ways.

Callsigns are stored in the callsign string CallSign[ ] in the following form:

Air navigation functions: data structure for callsign keying.

'pCallSign' keeps track of which character (byte) within the CallSign[ ] we are up to while processing the callsign.

The various kinds of radio navigation stations key their callsigns in different ways:

Non-Directional Beacons operate on the MF band sending out a 1020Hz continuous tone which is interrupted every 30 seconds to key the callsign. If we take the dura­tion of one Morse dot to be a fifth of a second (ie the letter 'A' or the letter 'N' will occupy one second, then the callsign coding for an NDB will be as follows:


Radio Range Stations send out a keyed 1020Hz tone. You hear a letter 'A' keyed continually if you are in the Northeast or Southwest quadrants of the station, and 'N' if you are in the Southeast or Northwest quadrants. This keying is interrupted every 30 seconds at which time the station's callsign is keyed twice. Sample call­sign cod­ing:


An Instrument Landing System keys its callsign 3 times in a row with a 1020Hz tone every 30 seconds. It prefixes the first of the 3 callsigns with the letter 'I' to indicate that it is an ILS. If it has an associated DME (Distance Measuring Equipment), the DME callsign is keyed after the 3 ILS callsigns. The DME callsign is keyed with a 3kHz tone. Sample callsign coding:


A VHF Omnidirectional Range station keys identically to an ILS except that it pre­fixes its first callsign with a 'V'.

A lone DME keys its callsign 3 times in a row every 30 seconds at 3kHz.

Airways Markers and Runway Back-Markers key their callsigns continually using a 3kHz tone. An Outer landing marker keys dashes continually using a 400 Hz tone, a Middle landing marker keys alternate dots and dashes continually using a 1.3kHz tone and an Inner landing marker keys dots continually using a 3kHz tone.

[4][1][1]T;			Outer marker
[4][1][1]A;			Middle marker
[4][1][1]I;			Inner marker

The Morse Code translation of a callsign segment is stored in Morse[ ] as follows:

Air navigation functions: data structure for a Morse code callsign.

'Morse[ ]' is a string in which each byte holds the duration of one of the Morse ele­ments (a dot, dash, inter-element space or inter-letter space) which make up the current callsign segment. Each duration is expressed as an 8-bit binary number of Morse-dot durations. The final duration in Morse[ ] is the inter-segment duration 'd' (see above). This is followed by a normal 'C' language 'null' string terminator '\0' which marks the current end of the string. 'pMorse' keeps track of which Morse ele­ment is currently being keyed.

'Single' is TRUE within the GS.DAT record of stations with single segment callsigns. A single segment callsign is translated into Morse code once only by SetStn() when the station comes into receivable range and is placed on the active stations list. A sta­tion with a multi-segment callsign has each segment of its callsign translated dyn­amically by the SetUpCallSign() function during the actual keying process.

The following function gets the next callsign segment from CallSign[ ] and sets it up as a string of Morse Code timer durations in Morse[ ]:

// 't' is a pointer to a Tx Data structure of the station currently being keyed

void SetUpCallSign(struct TxData *t) {

  /* Array letters of the Morse Code alphabet where
     the code for each letter is expressed as timer
     durations: dot=1, dash = 3. */

  char *MorseCode[] = {
    "\3\1", "\3\1\1\3", "\3\1\3\1", "\3\1\1", 
    "\1", "\1\1\3\1", "\3\3\1", "\1\1\1\1", 
    "\1\1", "\1\3\3\3", "\3\1\3", "\1\3\1\1", 
    "\3\3", "\3\1", "\3\3\3", "\1\3\3\1", "\3\3\1\3", 
    "\1\3\1", "\1\1\1", "\3", "\1\1\3", "\1\1\1\3", 
    "\1\3\3", "\3\1\1\3", "\3\1\3\3", "\3\3\1\1"
  char MC,             // Morse Code element (dot/dash/gap)
  *pMC,                // ptr to Morse element in MorseCode[ ]
  *pC = t->pCallSign,  // ptr to start of next callsign segment
  *pM = t->pMorse;     // ptr to start of stn's Morse string

  if(*pC == '\0')      // if we've reached end of callsign data
    pC = t->CallSign;  // reset ptr to start of callsign data

  t->Pitch = (int)*pC++;   // audio frequency of keying tone
  t->Repeat = (int)*pC++;  // Nº of times it is to be repeated		

  if((int delay = (int)*pC++) < 0) {  // if the delay is negative
    t->Key = TRUE;   // set morse key to the on state 
    delay = -delay;  // reverse its sign (make it positive)
  } else             // else delay is zero or positive
    t->Key = FALSE;  // so set morse key to the off state

  /* While the next call sign character is not a semicolon
     get pointer to next character's Morse sequence string. */

  while((char Letter = *pC) != ';') {
    pMC = MorseCode + Letter - 65;

    // For each Morse element (dot or dash) of the character

    while((MC = *pMC++) > '\0') {
      *pM++ = MC;    // store duration of this Morse element
      *pM++ = '\1';  // store duration of inter-element pause
    *--pM = '\3';    // overwrite with an inter-letter pause
    pC++;            // advance to next letter to be translated
  *pM++ = delay;        // overwrite with delay at end of callsign
  *pM = '\0';           // terminating null for Morse Buffer
  t->pCallSign = ++pC;  // start of next segment of callsign 

LF/MF Receiver Tuning

Tunable receivers use tuned circuits to select the station to be received.

A tuned circuit comprises an inductor (a coil of wire), a capacitor (two parallel con­ducting plates close to each other but not making electrical contact) and a resistor which simply resists the flow of electricity unconditionally. The resistor is not a sep­arate electrical component, but is mainly the inherent resistance of the wire of the coil.

The inductor and capacitor together may be imagined as the electrical equivalent of a pendulum with the resistor representing friction.

If you nudge a pendulum in time with its natural swing, it will sustain its maximum amplitude. The energy from your nudges will simply counterbalance the energy ab­sorbed by friction. If you nudge it too often or not often enough, then although it will still swing, the amplitude of the swing will be considerably less. The further away the frequency of the nudges is from the pendulum's natural frequency of swing, the more the amplitude of the swing diminishes.

When a radio signal from a station is fed into a receiver's tuned circuit, the signal is diminished. The amount by which it is diminished depends on how far off the fre­quency of the incoming signal (the nudges) is from the natural frequency (or swing) of the tuned circuit. The signal is diminished least when the station's frequency is the same as the natural frequency of the tuned circuit. Then, the only thing dim­inishing the signal is the resistance (the friction).

So a tuned circuit always impedes the passage of an electrical signal to some ex­tent. Its impedance Z to signals which oscillate at the tuned circuit's natural (or resonant) frequency is simply equal to its resistance R. The extra amount of im­pedance it imposes on signals which are not quite in tune with its resonant fre­qu­ency is called its reactance. The amount of the reactance X depends on how far the incoming signal is off-tune and is calculated as follows:

Air navigation functions: low and medium frequency receiver tuning simulation.

The frequency ω0 to which the receiver is tuned is varied by varying the value of the capacitor C. But in simulation, we do not know how the value of C is being var­ied. What we do know is the station's frequency and the receiver's tuned frequency. Knowing that the reactance of the tuned circuit to a signal with its resonant fre­quency is zero (impedance is resistance only), we can eliminate C as follows:

Air navigation functions: de-tuned signal strength calculation for low and medium frequency receivers.

The graph above shows how the attenuation factor A varies with frequency.

A 'C' function which returns the signal attenuation divisor, A, given station frequ­ency and receiver frequency is given below. It calculates the attenuation of a recei­ved signal when the receiver is (ω0 − ω) radians per second off tune as effected by 3 cascaded tuned circuits of Q = 100 as would be produced by the receiver's IF am­plifier chain.

double DeTune(long sf, long rf) {  // stn freq, rx freq
  #define L .0000003422   // tuned circuit's inductance in Henries
  #define R .01           // circuit resistance to give a Q = 100
    w = (double)sf,       // Compute the reactance X of single
    w0 = (double)rf,      // tuned circuit.
    X = L * (w - w0 * w0 / w),
    Z = X / R,            // impedance of single tuned circuit
    A = sqrt(1 + Z * Z);  // attenuation of single tuned circuit
  return(A * A * A);      // attenuation of 3 cascaded circuits

The way in which the signal strength — and hence audio level — which is presented to the user is simulated as follows:

Air navigation functions: where the de-tuned signal strength is presented in the receiver train.

Pass Band

Image of the de-tuning function generated by a tuned circuit simulator applet, which was formerly displayed live in the old Java-capable browsers. The graph of signal attenuation versus fre­quency for a particular tuned circuit ill­ust­rates what is called its pass-band. An ex­ample of such graphs showing the pass bands for 3 different circuits all tuned to 465kHz is generated on the right by a Java applet class called PassBand{}. It is ess­entially a Java version of the above C fun­ction 'DeTune()'. The red trace shows the off-tune signal attenuation produced by a single tuned circuit with a 'Q' of 1000. It is extremely sharp and is there­fore suitable for receiving narrow band transmissions like morse code.

The green trace shows the off-tune attenuation produced by passing the signal thr­o­ugh two tuned circuits in succession both of which have a 'Q' of 300.

The blue trace shows the effect of passing the signal through 4 tuned circuits in succession each of which has a 'Q' of only 100. This has a much broader peak mak­ing it suitable for speech and even music. It also provides much greater attenuation as you move away from the resonant frequency.

The source code for the above applet is available here. Please bear in mind that this was the very first applet I ever wrote in Java!

Circuit Configuration

This description of the tuning process has used a series tuned (or acceptor) circuit which is slightly easier to deal with mathematically. Receivers invariably use para­llel tuned (or rejecter) circuits. The values returned by DeTune() would be the same. Only a slightly more complicated formula using a parallel resistance constant r in­stead of the series resistance constant R.

Other Effects

A modulated signal from a station — whether carrying speech or a morse tone — comprises a central carrier flanked by lesser sideband signals. The beats or hetero­dynes between the carrier signal and the sideband signals are what produce the audio frequencies in the receiver.

When the receiver is slightly off-tune from a station, the carrier will be further away from the centre of the selectivity envelope than some of the sidebands. The carrier will therefore suffer greater attenuation than the sidebands. This results in the em­phasis and distortion of the higher audio frequencies, which is a familiar effect heard when a receiver is off-tune.

The DeTune() function does not cater for this effect, the simulation of which is left as an exercise and challenge for the reader!

ADF Tuning

The ADF receiver is a 3-band LF & MF receiver which has a separate control panel. The controller's tuning mechanism drives a synchro which turns the tuning capa­citor in the main unit located in another part of the aircraft.

For simulation we input the output of the controller's synchro straight into the com­puter via an AtoD SAMP (analogue-to-digital servo amplifier). We also input the pos­ition of the controller's band switch position as a logical input. The bands cover­ed by the ADF receiver are as follows:

Air navigation functions: the automatic direction finding (ADF) radio bands.

The ADF receiver's synchro input is expressed as an unsigned interger ranging from 0 to FFFF for one full turn of the synchro.

The frequency to which the ADF receiver is currently tuned can therefore be cal­c­ulated as shown in the following 'C' function:

long ADFfrq( struct RxData *s ) {
  int BandStart[]  = {189, 396, 830},  // kHz
      BandExtent[] = {221, 474, 990},  // kHz

      syn = r->Syn,       // current position of tuning synchro
      wb  = r->WB,        // currently selected waveband
      hi = r->SynHi[wb],  /* synchro position for high frequency
                                limit of the selected band */
      lo = r->SynLo[wb];  /* synchro position for low frequency
                                limit of the selected band */
  if(syn < lo) syn = lo;
  if(syn > hi) syn = hi;
  return((long)(BandStart + 
        ((syn - lo) / (hi - lo)) / BandExtent[wb]));

The extent of a frequency band is unlikely to cover a complete turn of the tuning synchro. In practice it will be less. Nor is it likely to start exactly at the synchro's zero position. It will start a little beyond. The values returned by the input word for the tuning synchro for a given receiver controller have to be determined by calibr­a­tion and entered into a fixed data file which is loaded on start-up. When the sim­u­lation program is started, this data is put into each receiver's RxData structure. Part of that data are the synchro input values for the upper and lower limits of each band for each receiver onboard the aircraft.

There is also an inevitable element of non-linearity in the frequency scales which ideally we should correct. This is not included in the above function but it would also be dealt with in the above function if it were dealt with at all.

Directional Information

The ADF receiver is intended for receiving non-directional beacons NDBs and radio range stations RRGs. It uses two loop aerials at right-angles to resolve the direction of a received station.

As well as an audio output, the ADF receiver has a synchro output which drives an ADF pointer on the aircraft's compass card showing the direction of the station as a compass bearing. This is how it navigates using the Non-Directional Beacons.

With a Radio Range Station, the pilot can tell which of the station's 4 quadrants he is in — north to east, east to south, south to west, west to north — according to whether he hears a Morse 'A' or a Morse 'N' being keyed. This allows the pilot to know roughly where he is in relation to the station even if the aircraft does not have dir­ection finding capability or if their ADF function is faulty.

Aircraft Receivers

Signals from radio-navigation stations are received by specialised radio re­ceivers on-board the aircraft.

There are generally 3 types of receiver as follows:

Air navigation functions: the 3 types of radio receiver generally used on an aircraft.

The RxData Structure

  int Syn;          // current position of tuning synchro
  int SynHi[3];     // synchro position for highest frequency in each band
  int SynLo[3];     // synchro position for lowest frequency in each band
  int WB;           // currently selected waveband
  int Sig;          // signal strength delivered by receiver's IF amp
  int Tone[3];      // tone levels delivered to receiver's AF stage input
  int Valid;        // NAV data validation bits
  long Frq;         // frequency to which receiver is currently tuned
  double ISR;       // selected inbound VOR radial or ILS runway heading
  double OSR;       // selected outbound VOR radial
  double Brg;       // bearing of tuned station from receiver
  double RadDev;    // deviation of aircraft from selected radial
  double StrOff;    // required steering offset from station
  double ReqHdg;    // heading the aircraft must fly
  double DistTD;    // ILS distance to touch-down
  double PrvDst;    // dist to/from stn at previous program pass
  double HdgErr;    // horizontal (VOR/ILS) deviation correction command
  double GsErr;     // aircraft's ILS glideslope deviation
  double DesErr;    // difference between required and actual angle of descent
  double Atten;     // signal attenuation divisor due to receiver being off-tune
  BOOL dFrq;        // set TRUE by Receivers() when receiver is re-tuned
  BOOL capture;     // indicates a selected radial has been captured
  BOOL OnOff;       // receiver's on/off switch
  BOOL CctBrk;      // receiver's d/c power supply circuit breaker
  BOOL SynBrk;      // receiver's synchro a/c power supply circuit breaker
  struct TxData *t; // pointer to the TxData structure of the
}                   // dominant currently on-tune station.
  *Rx[10];          // Array of pointers to NumRx receiver data structures 

To access the Rx[ ] pointer-array efficiently within for() loops we use a secondary array of pointers pRx[ ] which contains pointers to the elements of the Rx[ ] array:

Air navigation functions: The data structure used for aircraft receiver data.

pRx[ ] is declared inside the RxSig() function where it is used exclusively.

The following function determines the signal strength for all receivers of the type which receives the type of station whose TxData is pointed to by 't'. It also calcu­lates the possible separate audio tone levels for each of the three possible pitches of 1020Hz, 1300Hz and 3000Hz.

int RxSig(t) {
  int RxType[] = {  // Type of Stn received by Type of Rx
    0,              // 0       NDB              0      ADF
    0,              // 1       RRG              0      ADF
    1,              // 2       VOR              1      NAV
    1,              // 3     VOR/DME            1      NAV
    1,              // 4       ILS              1      NAV
    1,              // 5     ILS/DME            1      NAV
    1,              // 6       DME              1      NAV
    2,              // 7       AMK              2      MKR
    2,              // 8       BMK              2      MKR
    2,              // 9       OMK              2      MKR
    2,              // 10      MMK              2      MKR
    2               // 11      IMK              2      MKR

  // An array of pointers to pointers to RxData structures

  struct RxData
    **pRx[] = {
      Rx,      // points to Rx[] element ptg to RxData for ADF1
      Rx + 4,  // points to Rx[] element ptg to RxData for NAV1
      Rx + 8,  // points to Rx[] element ptg to RxData for MKR1
      Rx + 10  // points to the element beyond end of Rx array
    *r,       // points to RxData struct of rx being dealt with
    **rx,     // points to element of Rx[] containing 'r'

    **RX,     /* This pointer points to the element of Rx[] after the one
                 containing the pointer to last RxData structure for the
                 type of receiver concerned. */

    /* The final pointer points to the element within pRx[] (above) which
       points to the element within Rx[ ] which points to the RxData structure
       of the first receiver of the type which receives the type of station
       whose TxData structure is pointed to by 't'. */

    ***prx = pRx + *( RxType + t->StnType );

  /* FOR EACH RECEIVER OF THIS TYPE, IF Rx[rx] contains a pointer to an exist-
     ing RxData structure AND if this receiver has been re-tuned since the
     last station scan, THEN cancel the 'frequency changed' flag and get its
     detuning attenuation divisor. */

  for(rx = *prx, RX = *(prx + 1); rx < RX; rx++) {
    if(((r = *rx) > NULL)) && ((r->dFrq == TRUE)) {
      r->dFrq = FALSE;
      r->Atten = DeTune(t->Frq, r-Frq);

    /* Get the signal strength of this station at the aircraft and divide it
       by the de-tuning attenuation of receiver's passband. Rescale it from a
       floating point double to an interger ranging from 0 to 255. If the res-
       ulting signal is the strongest so far encountered during this station 
       scan THEN set it as receiver's current signal strength, and set this
       station as dominant on-tune station. */

    if((Sig = (int)(255 * TxSig(t)/r->Atten)) > r->Sig) {	
      r->Sig = Sig; r->t = t;

      /* If the station's Morse key is down, and this is the strongest station
         keying at this pitch, make this the sound level for this pitch of
         tone. */

      if((t->MorseKey == TRUE) && (r->Tone[t->pitch] < Sig))
        r->Tone[t->pitch] = Sig;

This function checks each receiver's on/off switch and dc power circuit breaker and then determines the frequency to which it is currently tuned. If the receiver has been retuned since the last pass, the new frequency is stored and a 'frequency changed' flag is set in the receiver's RxData structure. If the receiver is switched off or its circuit breaker is out, its frequency is set to zero. The signal and tone levels of all receivers are reset to zero ready for the next station scan.

void RxInput() {
  long(*GetFrq[])(struct RxData *) = {
    ADFfrq, ADFfrq,  // function returning ADF rx frequency
    ADFfrq, ADFfrq,
    NAVfrq, NAVfrq,  // function returning NAV rx frequency
    NAVfrq, NAVfrq,
    MKRfrq,MKRfrq    // function returning Marker frequency
  struct RxData *r;  // points to RxData of current rx
    **rx;            // ptr to Rx[] element holding 'r'
    **RX = Rx + 10;
  static int r = 0;  // element number of Rx[] range 0 to 9
  long frq;          // current receiver frequency
  register n = 0;    // element number of Rx[]

  for(rx = Rx; rx <= RX; rx++, n++) {  // for all possible receivers
    if((r = *rx) > NULL) {    // if an rx is installed in this 'position'
      if(r->CctBrk == TRUE    // if rx's dc circuit breaker closed
      && r->OnOff == TRUE) {  // and rx is switched on
        if((frq = (*(GetFrq + rx))(r)) != r->Frq) {  // if rx has been retuned
          r->Frq = frq;       // store new frequency
          r->dFrq = TRUE;     // indicate that it has changed
      } else r->Frq = 0;      // frequency = 0 means rx inoperative
    r->Sig = 0;               // clear the dominant signal strength
    for(register x = 0; x < 3; x++)
      r->Tone[x] = 0;         // clear the Morse tone levels

Transmitter Simulation

The function below simulates the activity of all active stations by controlling the key­ing of their callsigns and computing their signal strengths and audio tone levels presented to each receiver.

Whenever a station's Morse tone duration timer expires, the Transmitters() function reloads the station's Morse tone duration timer with the duration of the next dot, dash, inter-element, inter-letter or delay specified in its callsign string and flips the state of the Morse key from tone-on to tone-off or vice versa. This function should be called roughly every 200 milliseconds.

void TxScan() {                 // NAVIGATION STATION SCANNER
  static double Sigma, InvDst;  // For computing height of 
  static int N;                 // ground under aircraft.
  static struct TxData **tx;    // Pointer to an element in 
                                // Tx[ ] pointer array.
  struct TxData *t;             // Pointer to a TxData (ie a 
                                // station data) structure.

  for(tx = Tx; tx <= TxHi; tx++) {   // for each element in Tx[] pointer array
    if(((t = *tx) > NULL)            // If it contains a ptr to a TxData struct
    && (--(t->MorseTimer) < 0)) {    // & it's the end of current Morse element
      t->MorseKey = !(t->MorseKey);  // reverse the state of the Morse key.
      if(*(t->pMorse) == '\0') {     // if end of current dots/dashes sequence
        t->pMorse = t->Morse;        // reset ptr to start of Morse Code buffer.
        if(t->Single == FALSE)       // if station has more than 1 callsig
        && ((--(t->Repeat) < 0))     // avoid possible division by zero
          SetUpCallSign(t);          // get the next part of the call sign
      t->MorseTimer = *(t->pMorse)++;  // Set timer to duration of next dash/dot
    RxSig(t);                   // compute stn's signal strength for each rx
    if((t->StnType == 4         // if the station is an ILS
    || t->StnType == 5)         // or an ILS/DME
    && (t->Dst > 0)) {          // and its distance is > zero
      InvDst += 1 / t->Dst;     // add inverse dist
      Sigma += t->Ht / t->Dst;  // add dist/height
  Air.GndHt = Sigma / (N * InvDst);  // Compute estimated height of ground 
}                                    // beneath aircraft

Way Point Navigation

As well as receiving Morse signals from radio navigation stations, the air­craft's receivers also derive navigational information from them.

ADF Output

ADF receivers use special aerials comprising two coils at right angles to each other to resolve the direction a received signal is coming from. This gives the pilot the dir­ection of the received station from the aircraft.

Air navigation functions: the range and bearting geometry for NDB (non-directional beacon) and RRG (radio range) stations. The bearing calculated by TxSig() is the bearing of the aircraft from the station. In spherical geometry, the bearing of the station from the aircraft is not just 180 degrees minus the bearing of the aircraft from the station as it would be on a flat landscape. Conse­quently, the ADF bearing must be computed separ­ately by the DandB() function with the swap flag set to TRUE.

r->Brg = DandB( t, MaxDst, TRUE );

The direction of a station being received by the ADF receiver is then presented by a radio direction needle on the compass instrument:

Air navigation functions: the compass card relative bearing indicator.

VOR Output

VOR stations emit one signal which has the same phase in all directions and an­other signal whose phase angle varies as the direction of emission. For example, if you are south-east of the VOR station, the second signal will be 135 degrees out of phase with the first signal. By detecting the phase difference between the two signals it receives, the NAV receiver determines the aircraft's bearing from the station, t->Brg.

A VOR station is placed at an airways junction or waypoint at which a change of course is necessary. The pilot selects the radial r->ISR on which he wishes to ap­proach the station with the intention of intercepting that radial, flying along it towards the station, and then onwards from the station along the opposite radial.

Navigation Computer

The values of r->ISR and t->Brg are passed to the navigation computer which der­ives from these the heading the aircraft must fly, r->ReqHdg, in order for the air­craft to make a smooth turn on to the radial so that it is travelling in line with the radial as it passes over the station.

The following diagram shows the pilot to have selected radial 300 (the line on com­pass bearing 300 which goes slightly north of west out from the station). So he intends to fly on his present heading of approximately north-east and then turn eastwards when he intercepts the station's Radial 300.

Air navigation functions: the pre-capture geometry for a selected radial of a VHF omni-directional range (VOR) station.

The pilot selects the radial he requires, r->ISR, by entering it in via his keyboard or dial switches. The NAV receiver provides the aircraft's current bearing from the VOR station, t->Brg and its current distance from the station, t->Dst.

It is uneconomic for the aircraft to start approaching the radial when it is still a long way out from the station. The navigation computer therefore maintains the value of r->ReqHdg so as to keep the aircraft flying towards the station along a straight course which is tangential to one of what are called the station's 'Circles of Cap­ture'. These are set to have a radius R equal to that of the aircraft's smallest com­fortable turning circle.

The navigation computer therefore computes the heading r->ReqHdg which the aircraft must fly by working out the steering offset r->StrOff from the station bear­ing r->Brg which will keep the aircraft aimed at the edge of the circle on the side of the selected radial closest to the aircraft. When the aircraft reaches the edge of the circle, it follows the circumference on to the selected radial, r->ISR until it reaches the station. It thus intercepts the selected radial at the station. At this point it is said to have captured the radial.

Once the aircraft has passed over the station, it must fly along outbound radial, r->OSR, the 180º projection of the inbound selected radial, r->ISR. The required heading, r->ReqHdg must be maintained to bring the aircraft smoothly back on to r->OSR whenever it drifts off. After capture, the navigation computer therefore det­ermines r->ReqHdg in a different way as follows:

Air navigation functions: the post-capture geometry for a selected radial of a VHF omni-directional range (VOR) station.

Being referenced to r->Brg, the bearing of the station from the point of view of the aircraft, the above formula takes account of spherical geometry and so works at long distance as well as short distance.

A slight problem is that when the aircraft passes over the station, it always in fact passes it a small but nonetheless finite distance to one side or the other causing r->Brg to whip suddenly through 180º. The resulting value of r->ReqHdg from the above formula therefore causes the aircraft to leave the station in what can am­ount to any direction. This is overcome by imposing a reasonable upper limit on the steering offset r->StrOff.

The GetReqHdg() Function

The following 'C' function simulates the navigation computer's function which com­putes the required heading. The required heading r->ReqHdg has nothing to do with the aircraft's actual current heading.

double GetReqHdg(struct TxData *t, struct RxData *r) {
  #define R 40     // radius of aircraft's minimum turning circle
  #define RR 1600  // square of this radius
  double a,        // used for intermediate angular quantities
    b, c, e,       // sides of construction triangles see diagram
    d = t->Dst;    // distance between aircraft and waypoint
  BOOL f = FALSE;  // to indicate whether to use + or − root

  /*If the aircraft has not so far reached the station and it is now closer
    than (within) the radius of the minimum turning circle and it is now
    receding from the station then it is deemed now to have passed station. */

  if(r->capture == FALSE && d < R && d > t->PrevDst)
    r->capture = TRUE;

  /*If the aircraft has not yet reached the station then get its deviation
    from the selected radial, determine which side of the radial it is, and
    finally find b and c (see Pre-capture diagram).*/

  if(r->capture == FALSE) {
    r->RadDev = (a = BipAng(t->Brg - r->ISR));
    if (a < 0) { a += Pi; f = TRUE; }
    b = R * cos(a); c = d - R * sin(a); 

    /*If the aircraft is outside the minimum turning circle compute the square
      root of the appropriate sign, then compute the rationalised required
      heading it must fly.*/

    if((e = b * b + c * c - RR) > 0) {
      e = sqrt(e); if(f == TRUE) e = -e;
      r->ReqHdg = RatAng(r->Brg + (r->StrOff = atan(R/e)- atan(b/c)));

  } else {  // else the aircraft has passed the station

    // compute a and b (see the Post-capture diagram).
    r->RadDev = (a = BipAng(t->Brg - r->OSR));
    a = r->Brg - a - a - Pi;

    r->StrOff = (b = BipAng(r->OSR - a));  // Get steering offset
    if (b > +1) a = r->OSR + 1;            // and limit it to +/-
    if (b < -1) a = r->OSR - 1;            // one radian.
    r->ReqHdg = RatAng(a);
  return(r->ReqHdg);  // return the aircraft's required heading

When adding and subtracting a number of angles, some of which could be compass bearings and some of which could be deviations from reference lines, we can end up with very large angular quantities which can be positive or negative. Many math­ematical functions can only accept angles from 0 to 2pi or from -pi/2 to +pi/2. So we need to be able to rationalise and polarise angles.

These two tasks are done by RatAng() and BipAng() below which should appear be­fore (or at least prototyped before) GetReqHdg():

// Put a wild angle in range 0 to 2pi
double RatAng(double theta) {
  while(theta < 0) theta += TwoPi;
  while(theta > TwoPi) theta -= TwoPi;

// Make a wild angle bipolar
double BipAng(double theta) {
  while(theta < -Pi) theta += TwoPi;
  while(theta > Pi) theta -= TwoPi;

Although not used in GetReqHdg(), later on we will also need to be able to make sure that an angle such as a corrective deviation cannot exceed an operating limit. This is done by LimAng(), which for completeness is shown now below:

// Impose an upper and lower limit on a bipolar angle
double LimAng(double theta, double limit) {
  if(theta < 0) {
    if(theta < -limit) theta = -limit;
  } else if(theta > limit) theta = limit;

Exerciser for GetReqHdg()

To prove that GetReqHdg() will correctly give the heading the aircraft must follow to bring it to a smooth intercept of the selected radial, the following Microsoft Win­dows program uses the same formulae to trace out the trajectory of the aircraft for every possible radial from 0 to 360°.

Use the generic Microsoft Windows program Output.C in the Windows SDK Guide To Program­ming. Define and declare the RxData and TxData structures Rx and Tx plus the following at the beginning of the source file:

HPEN hBlackPen, hWhitePen, hYellowPen;
  HBRUSH hWhiteBrush, hBlueBrush;
  int th;         // text height (leading)

In MainWndProc(), modify the WM_PAINT, WM_CREATE and WM_DESTROY cases as follows:

case WM_PAINT:
    hDC = BeginPaint(hWnd, &ps);
    GndTrk(hWnd, hDC);
    EndPaint(hWnd, &ps);

  case WM_CREATE:
    hBlackPen   = GetStockObject(BLACK_PEN);
    hWhitePen   = CreatePen(PS_SOLID, 3, RGB(255,255,255));
    hYellowPen  = CreatePen(PS_SOLID, 1, RGB(200,200,0));
    hWhiteBrush = GetStockObject(WHITE_BRUSH);
    hBlueBrush  = CreateSolidBrush(RGB(0,0,119));
  case WM_DESTROY:

Then add the following at the end of the generic source file:

  #define Pi 3.1415926535
  #define TwoPi 6.2831853070
  #define PiBy2 1.5707963267
  #define DegRad 57.29577	// No of degrees in a radian

Put RatAng() and BipAng() in here followed by the digital read-outs display function:

ShowDeg(HDC hDC, int n, double a) {
  extern int th;
  RECT rcTextBox;
  char s[8];
  int i;
  if (n < 12)                    // If not displaying distance
    a *= DegRad;                 // then convert to degrees 
  i = a; if ((a -= i) > .5) i++;
  sprintf(s, " %3.3d", i);
    &rcTextBox, 120, 10 + th * n, 170, TOP + th * (n + 1)
  DrawText(hDC, s,strlen(s), &rcTextBox, 

Finally, put in GetReqHdg()'s exerciser, GndTrk():

void GndTrk(HWND hWnd, HDC hDC) {
  #define X 400    // in logical pixels
  #define Y 152    // in logical pixels
  #define TD 140   // distance of touchdown beyond station
  #define R 40     // Radius of turning circle
  extern int th;
  extern HPEN hBlackPen, hWhitePen, hYellowPen;
  extern HBRUSH hWhiteBrush, hBlueBrush;
  struct TxData *t = &Tx;  // can't pass externally-declared ptrs
  struct RxData *r = &Rx;  // internally-declared ptrs are auto!
  int SelRad, x, y;        // SR in degrees, co-ords of aircraft
  double a, b, isr, xx, yy, x1, y1, x2, y2;
  char s[24], ToFrom;
  GetTextMetrics(hDC, &tm);
  th = tm.tmExternalLeading + tm.tmHeight;
  y  = 10; strcpy(s,"AIRCRAFT TRACK FOR");
  TextOut(hDC, 10, y, s, strlen(s));
  y += th; strcpy(s,"AUTOMATIC CAPTURE "); 
  TextOut(hDC, 10, y, s, strlen(s)); 
  y += th; strcpy(s,"OF WAYPOINT RADIAL"); 
  TextOut(hDC, 10, y, s, strlen(s)); 
  y += th;
  y += th; strcpy(s,"Selected Radial" ); 
  TextOut(hDC, 10, y, s, strlen(s));
  y += th; strcpy(s,"Aircraft Bearing"); 
  TextOut(hDC, 10, y, s, strlen(s)); 
  y += th; strcpy(s,"Radial Deviation"); 
  TextOut(hDC, 10, y, s, strlen(s));
  y += th;
  y += th; strcpy(s,"Station Bearing" ); 
  TextOut(hDC, 10, y, s, strlen(s));
  y += th; strcpy(s,"Steering Offset" ); 
  TextOut(hDC, 10, y, s, strlen(s));
  y += th; strcpy(s,"Required Heading"); 
  TextOut(hDC, 10, y, s, strlen(s));
  y += th;
  y += th; strcpy(s,"Distance"); 
  TextOut(hDC, 10, y, s, strlen(s));
  for(SelRad = 0; SelRad <= 360; SelRad++) {  // for each of the 360 degrees
    r->ISR = (isr = SelRad / DegRad);
    r->OSR = RatAng(isr + Pi);
    SelectObject(hDC, hBlackPen);
    SelectObject(hDC, hBlueBrush);
    Rectangle(hDC, X - 220, Y - 150, X + 150, Y + 150);
    a = isr + PiBy2; x1 = R * sin(a); y1 = -R * cos(a);
    a = isr - PiBy2; x2 = R * sin(a); y2 = -R * cos(a);
    for (b = 0; b <= TwoPi; b += .017453292) {
      xx = X + R * sin(b); yy = Y + R * cos(b);
      SetPixel(hDC, x = x1 + xx, y = y1 + yy, RGB(255,0,0));
      SetPixel(hDC, x = x2 + xx, y = y2 + yy, RGB(0,255,0));
    SelectObject(hDC, hYellowPen);
    x = TD * sin(isr); y = -TD * cos(isr);
    MoveTo(hDC, X - x, Y - y);
    LineTo(hDC, X + x / 2, Y + y / 2);
    SelectObject(hDC, hBlackPen);
    SelectObject(hDC, hWhiteBrush);
    Ellipse(hDC, X - 5, Y - 5, X + 5, Y + 5);
    xx = -210; yy = 0;          // starting position of aircraft
    t->PrevDst = (t->Dst = sqrt(xx * xx + yy * yy) + 1) + 1;
    SelectObject(hDC, hWhitePen);
    MoveTo(hDC, x = X + xx, y = Y + yy);// move to starting posn
    r->capture = FALSE;         // not yet reached the station
    while(((a = t->Dst) < t->PrevDst && a > R) || a < TD) {
      t->PrevDst = a;
      t->Dst = sqrt(xx * xx + yy * yy); //initial dist from stn
      r->Brg = RatAng((t->Brg = RatAng(atan2(xx, yy))) + Pi);
      xx += sin(a = GetReqHdg(t, r));  // update aircraft's posn
      yy += cos(a);
      LineTo(hDC, x = X + xx, y = Y - yy); // plot its position
                                           // on the screen
      ShowDeg(hDC,  4, r->ISR);
      ShowDeg(hDC,  5, t->Brg);
      ShowDeg(hDC,  6, r->RadDev);         // Radial Deviation
      ShowDeg(hDC,  8, r->Brg);
      ShowDeg(hDC,  9, r->StrOff);
      ShowDeg(hDC, 10, r->ReqHdg);
      a = t->Dst;
      if (r->capture == FALSE) a = -a;
      ShowDeg(hDC, 12, a);
    for(b = 0; b < 3000; b++);             // delay

I have since implemented this demonstration as a Java applet.

Aircraft Position and Motion

We now have to write some functions to deal with both the linear and rot­ational motion of the aircraft. To do this we first need a time reference.

Elapsed Time

It has been assumed that this software is called every 200 milliseconds. In a Micro­soft Windows environment, however, it has been found that timer messages and call-backs can be most irregular and missed out altogether. Time-dependent opera­tions invol­ving speed and rate of turn and such like must be computed with refer­ence to the true elapsed time since the previous pass of the program.

PC system time in Unix-based operating systems is provided in a time structure which contains among other quantities a double containing the number of seconds which have elapsed since the 1st January 1970 [1980 in Microsoft Windows], and an int containing the number milliseconds through the current second. In the func­tion below we access these and combine them into a double giving the number of sec­onds (to 1 millisecond precision) which have elapsed since the last time the function was called.

double ElapsedTime() {    // elapsed time since previous pass
  static struct timeb T;  // System time at last program pass
  static double 
    PrevTime = 0;         // absolute system time at last pass
    ElapsedTime = 0;      // elapsed time since previous pass
  double CurrentTime;     // absolute system time this pass
  ftime(&T);              // get current system time

  CurrentTime = T.time + ( (double)T.millitm ) / 1000;
  if(PrevTime > 0) ElapsedTime = CurrentTime - PrevTime;

  PrevTime = CurrentTime; // previous time ready for next pass
  return(ElapsedTime);    // return number of seconds +- 1ms

Rate of Turn

To be able to turn the aircraft smoothly on to its required heading, the navigation computer instructs the autopilot to roll the aircraft to give it an appropriate rate-of-turn. An aircraft's rate-of-turn is essentially a function of its roll angle — how far it is tilted sideways. A simplified view of how an appropriate rate-of-turn is determined from a given heading error is as follows:

Air navigation functions: geometry of the calculation of an aircraft's rate of turn.

The following function stands in place of a full autopilot, airframe, engines and fli­ght dynamics simulation for the type of aircraft concerned. We pass to it the cur­rent heading error (HdgErr) and the elapsed time (et) since it was last called, and it re­turns the aircraft's average rate-of-turn since it was last called.

double GetRot(double et, double HdgErr) {  // get rate of turn

  #define RollHdgErr 0.03  // Required roll per unit hdg error
  #define RollMax    0.03  // Max allowed roll angle (radians)
  #define RollDamp 0.02    // Rate-of-change of roll damping factor

  // Rate-of-turn per unit roll (radians per second per radian)
  #define RotRoll  1.00

  double dRoll = RollDamp * et 
               * (LimAng(RollHdgErr * HdgErr, RollMax) − Air.Roll);

  Air.Roll += dRoll;       // update aircraft's roll angle

  // Return average rate of turn since last program pass
  return(RotRoll * BipAng(Air.Roll - dRoll / 2));

Instrument Deviation

A quick aside here to define an ancillary function which is used later in the global position calculation.

A heading error is presented to the pilot as an instrument needle deviation. This deviation is made to be non-linear to make the reading more sensitive the smaller the error. The non-linear formula used is y = sin( Err / 2 ) shown below:

Air navigation functions: the heading-deviation indicator function.

The following function uses this formula to return the appropriate instrument need­le deviation (on a -100 : 0 : +100 scale) for a given angular error in the range from -π to +π:

int GetDev(double theta) {       // Get Deviation Angle
  ((int)(100 * sin(theta / 2));  // scaling factor -100 to +100

This same function is used also for presenting the aircraft's glideslope error, GsErr, to show how far the aircraft is off the glideslope in the vertical plane.

Global Position

Air navigation functions: the global position update calculation.

The function below in effect simulates the action of a basic GPS receiver. Given the aircraft's current position, speed and heading, it computes the aircraft's new posi­tion, heading and rate-of-turn based on the time that has elapsed since the prev­ious program pass.

/* Compute aircraft's heading error and store
   it as a non-linear instrument deviation. */

void HorzSteer(struct TxData *t, struct RxData *r, double et) {

  r->HdgErr = GetDev(double HdgErr = BipAng(ReqHdg(t, r) - Air.Hdg));

  /*Compute & store aircraft's average rate-of-turn and from that compute its
    incremental change in heading since last time this function was called.*/

  double dHdg = (Air.Rot = GetRot(et, HdgErr)) * et;

  double AveHdg = Air.Hdg + dHdg / 2;  /* Compute aircraft's average heading
                                          since last program pass. */
  Air.Hdg = RatAng(Air.Hdg + dHdg);    // Update the aircraft's actual heading
  double dDst = Air.Spd * et;          // Compute distance aircraft has moved
                                          since last program pass. */
  double dLat = dDst * cos(AveHdg);    /* Compute change in aircraft's
                                          latitude since last pass. */

  Air.Lat += dLat;  // Update aircraft's latitude and longitude
  Air.Lng += dDst * sin(AveHdg) / cos(Air.Lat + dLat / 2);

Air.Spd is in Earth-radians per second so d * sin(Air.Hdg) is in Earth-radians. Divid­ing this by cos(average latitude of aircraft during time of travel) gives the true long­itude increment. Air.Spd is the true ground speed and Air.Hdg is the true direction in which the aircraft is moving along its ground track. This need not be the direction the aircraft is pointing since wind may be causing sideways drift.

Ground Height Computation

The estimated ground height under the aircraft is derived from the ground heights of local ILS stations or height reference features. The significance of a particular ILS station's height is deemed to be inversely proportional to its distance from the air­craft: ie the nearer the ILS station is to the aircraft, the more relevant is its height for estimating the height of the ground beneath the aircraft.

Air navigation functions: the ground height calculation.

The following ground height function is illustrative only. The ground height compu­ta­tion is in fact built in as part of the function TxScan().

void GndHt(
  struct TxData *t,             // pointer to this station's data structure
  int n                         // total number of in-range stations
) {
  static double Sigma, InvDst;  // summation variables
  static int N;                 // ILS station count
  N++;                          // increment Nº of ILS stns considered
  if(t->Dst == 0)               // if aircraft directly over an ILS stn
    Air.GndHt = t->Ht;          // height of ground = height of station
  else {                        // else continue the summations
    InvDst += 1 / t->Dst;
    Sigma += t->Ht / t->Dst;
    Air.GndHt = Sigma / (N * InvDst);

Attitude Reference System

To navigate effectively, an aircraft's flight control system must know its pitch, roll and bearing relative to the ground. This is provided by a device containing one or more spinning flywheels equipped with tilt sensors.

Gyro Platform

Air navigation functions: schematic of the attitude reference gyro platform.

The bottom synchro is a stable gyro reference platform upon which rests an error synchro which corrects the compass card to the Earth's magnetic field. The card position is monitored by the upper synchro which takes into account the fact that the card can be turned manually as well as by the error synchro. Gyrocompass is simulated by the following 'C' function (excluding circuit breaker logic):

void Gyro(double et) {
  #define T       ????          // annunciator's tick-tock constant
  #define FSD     ????          // annunciator's full-scale deflection
  #define HALFDEG .008725       // ½° in radians
  #define ERRDEC .002908333     /* 10°/min card error decrement constant
                                   in radians/second */
  static int AnnDecr = T;       /* Use Annunciator tick-tock constant
                                   as iterative decrement */

  /* If the compass card is within half a degree of indicating the correct
     magnetic heading, make the annunciator tick-tock, otherwise set it to
     full scale deflection. */

  if((double CardErr = BipAng(Air.CardHdg - Air.Hdg - Air.MagVar)) < HALFDEG) {
    if(Air.Ann > +FSD)          // if annunciator > full scale +ve 
      AnnDecr = -T;             // make deflection decrementer -ve
    else if(Air.Ann < -FSD)     // if annunciator < full scale -ve
      AnnDecr = +T;             // make deflection decrementer +ve
    Air.Ann += AnnDecr * et;    // Decrement annunciator by the 
  }                             // amount of the tick-tock constant
  else Ann = FSD;               // else set annunciator to full scale deflection
  double ErrDec = ERRDEC * et;  /* The amount by which to decrement
                                   the card error this program pass */
  if(CardErr > 0)               // a +ve error requires a -ve 
    ErrDec = -ErrDec;           // decrement and vice versa
  Air.CardHdg += ErrDec;        // advance card heading towards magnetic heading

Pitch & Roll

Pitch and roll information is provided by 3 gyro platforms:

  System 0   the auxiliary gyro
  System 1   the captain's main gyro
  System 2   the first officer's main gyro

The auxiliary can be switched in place of either of the other gyros. The transfer swi­tch is a 28 volt dc relay supplied through its own circuit breaker. Each gyro is pow­er­ed from the appropriate 115 volt ac bus via its own circuit breaker.

The captain and first officer each have an attitude reference indicator containing a sphere which shows the current attitude (pitch and roll) of the aircraft. These are each powered through their own circuit breakers from the appropriate 115 volt ac bus. If an instrument or the gyro supplying it with attitude information is off or faulty, a physical warning flag drops into view within the instrument window.

Air navigation functions: graph of the precess test cycle of the attitude reference gyro platform. The captain or first officer can check the integ­rity of his gyro and instrument synchros by put­ting it through a 'precess' cycle. This causes the sphere to precess exponentially from the level-flight indication to an indication of 90-degrees of both pitch and roll. The precess cycle, as ill­ust­rated by the adjacent graph, takes about 10 sec­onds to complete.

Assuming the equipment is working properly, the sphere then drops back to indicate the air­craft's current pitch and roll. The precess cycle is controlled by the precess state flag as foll­ows:

GyroState = 0  gyro just switched on or has been warm-reset
            1  gyro has erected and is working OK
            2  gyro has failed

The following data has to be maintained independently for each gyro system. The data structures for the auxiliary gyro (system 0), the captain's main gyro (system 1) and the first officer's main gyro (system 2) are accessed through the array of pointers *Gx[ ]. Data relating to the horizon instruments is in the captain's (NAV1) or the first officer's (NAV2) RxData structure as appropriate.

struct GyroData {
  int Status;     // stage in the gyro precess cycle (see above)
  double Pre[2];  // current gyro pitch & roll precess angles
  BOOL GyroCB;    // gyro's 115volt a.c. circuit breaker state
  BOOL Erect;     // TRUE = gyro erected OK, FALSE = gyro failed
} *Gx[3];

A 'C' function which simulates the gyro's precessing cycle is shown below. This func­tion is entered separately for pitch and for roll for each gyro system.

double GyroPrecess(
  struct GyroData *g,          // pointer to data struct of Gyro 0, 1, 2
  int i,                       // pitch/roll index:  0 = pitch, 1 = roll
  double et                    // elapsed time since last program pass
) {
  #define INIT  .0001          // gyro angle at start of precess cycle
  #define K    1.5             // precess iterative multiplying factor
  double pre = *(g->Pre + i);  // gyro's current precess angle

  if(g->Status == 0) {  // If this gyro has just been switched on or reset
    pre = INIT;         // set its precess angle to its initial 'seed' value
    g->Status = 1;      // advance the gyro to its 'precessing' state
  else                  // Otherwise
  if((g->Status == 1)   // if this gyro is currently in its precessing cycle
  && (pre < HalfPi)) {  // and it has not yet precessed 90 degrees
    pre *= (K * et);    // multiply its current precess angle by 90 degrees
    if(pre > HalfPi) {  // If, as a result, the precess angle has now over-
      pre = HalfPi;     // shot its 90° limit, set it to its 90° limit and
      g->Status = 2;    // advance the gyro to its 'precess completed' state
  *(g->Pre + i) = pre;  // Store and return the updated precess 
  return(pre);          // angle ready for next pass

The circuit breaker and main/auxiliary gyro switching logic is dealt with by the foll­owing function. It returns an index number 'j' which indicates which gyro platform is supplying the attitude information. If we are dealing with the captain's instruments, j can be 0 or 1; if we are dealing with the first officer's instruments, j can be 0 or 2 according to whether the pilot concerned is switched to the auxiliary or his main gyro. However, if the gyro is inoperative for any reason, it returns a negative value of 'j' according to the nature of the problem.

int GyroLogic(
  int i,        // index: 0 for captain's or 1 for first officer's instruments
  double et     // elapsed time since last program pass
) {
  struct RxData *r = *(Rx + 4 + i);  /* Declare pointer to the captain's
                                        or first officer's RxData */

  if(r->TranCB == TRUE)      // If 'transfer switch' circuit breaker closed
  && (r->TranSw == True)) {  // & transfer switch is set to 'auxiliary gyro'
    r->TranLmp = TRUE;       // light the 'transferred to auxiliary' lamp
    j = 0;                   // set gyro index = 0 [for auxiliary gyro]
  } else {                   // Else
    r->TranLmp = FALSE;      // kill the 'transferred to auxiliary' lamp
    j = i;                   // set the gyro's index the same as
  }                          // the captain's or first officer's

  struct GyroData *g = *(Gx + j);  /* Declare a pointer to the data
                                      structure for the gyro in use. */

  if(g->GyroCB == FALSE)  // If relevant gyro's circuit breaker is out, set
    j =  -1;              // gyro index to show gyro platform inoperative.
  if(r->SphCB == FALSE    // if horizon circuit breaker is out 
  || g->Erect == FALSE)   // or gyro platform failed to erect
    j = -2;               // set gyro index to show attitude system failed
  return(j);              // return valid gyro platform index or failure code.

The main attitude system function below uses the previous two functions to provide the attitude (pitch and roll) indications on the captain's or first officer's horizon display, and also displays the appropriate instrument warning flags and lights the appropriate warning lamps.

void AttitudeSystem (
int i,     // index: 0 = captain's or 1 = first officer's instruments
double et  // elapsed time since last program pass
) {
  if((int j = GyroLogic(i, et)) < -1) {  // IF CURRENT GYRO IS INOPERABLE
    r->GyroFlag = TRUE;                  // display its warning flag
    if(j < -1)                           // If gyro platform failed 
       r->AttFailLmp = TRUE;             // light attitude system fail lamp
  } else {                               // ELSE THE GYRO CONCERNED IS WORKING
    r->AttFailLmp = FALSE;               // kill attitude system fail lamp
    r->Pitch = Air.Pitch;                // set sphere pitch to aircraft pitch
    r->Roll = Air.Roll;                  // set sphere roll to aircraft roll

    // If the pilot concerned is holding down 'precess test' button, then
    if(r->PreReq == TRUE) {
      r->GyroFlag = TRUE;                // show the gyro warning flag
      r->Pitch += GyroPrecess(g,0,et);   // add gyro precess pitch increment
      r->Roll += GyroPrecess(g,1,et);    // add gyro precess roll increment
    } else r->GyroFlag = FALSE;          // else hide warning flag

Landing Approach

At an airport, information in the horizontal plane is provided by an ILS localiser transmitter which is positioned at the opposite end of the run­way from the point of touchdown.

ILS Glide Slope

The ILS functions like a VOR with the 'selected radial' permanently set to the run­way heading. The angular error by which the aircraft is off the runway's extended centre line is called the localiser deviation. It is the same thing as the horizontal deviation from a VOR's selected radial, Hdev.

Air navigation functions: the ILS localiser approach geometry.

To calculate the glideslope deviation we must first transfer the point of reference (from which we determine the aircraft's position) from the localiser to the point of touchdown. This is to determine the distance 'x', which is the aircraft's effective distance out along the extended runway centre line.

We then switch to the vertical view and use 'x' and the aircraft's height to find the aircraft's current angle-of-approach to the point of touchdown. We then compare this with the runway's prescribed glideslope angle to find the glideslope error.

The glide slope transmitter near the runway touch-down point sends up a pair of radio beams at angles such that they have equal strength along the plane of the approach glide slope. The plane of the glide slope tilts upwards at an appropriate glide slope angle 't->GS' from the point of touch-down on the runway:

Air navigation functions: the ILS glide-slope approach geometry.

The glide slope error, GsErr is the angular difference (a - t->GS) by which the air­craft is above or below the glide-slope plane. This glideslope error signal is received by the NAV receiver when it is within 30km of the glideslope transmitter.

To be able automatically to bring the aircraft on to the glide slope smoothly we must calculate the 'required angle of descent' as shown in the above diagram:

double ReqDes(struct TxData *t, struct RxData *r) {
  #define FlairHeight .0000015  // ~9.55 metres in Earth radians
    Hdev,             // aircraft's angular displacement from runway centre line
    ReqDes,           // Required angle of descent
    gs,               // runway's glideslope angle
    GsErr,            // linear version of glideslope error
    RunLen = t->Len;  // runway length

  if(Air.Ht < FlairHeight) {  // if aircraft is now below flairout height
    RunLen *= .75;            // advance touchdown point 1/4 way down runway
    gs = 0;                   // and set glideslope angle to 0
  } else gs = t->GS;          // else, use runway's actual glideslope angle

  Hdev = BipAng(t->Brg - r->ISR);  /* Compute aircraft's angular displace-
                                      ment from runway centre line. */

  /*Provided aircraft's distance along the extended runway
    centre line from current point of touchdown is non-zero: */

  if((double x = (double D = t->Dst) * cos(Hdev)) - RunLen) != 0) {

    /* Compute the glide slope error as a linear angular quantity and store
       it in RxData as an appropriately scaled instrument needle deviation: */

    r->GsErr = GetDev(GsErr = BipAng(double a = atan(Air.Ht / x)) - gs));

    double y = D * sin(Hdev);        // compute perpendicular dist. to runway
    r->DstTD = sqrt(x * x + y * y);  // line & distance to touchdown point
    ReqDes = BipAng(a + GsErr);      // required angle of descent
  } else {                           // ELSE, DISTANCE TO TOUCHDOWN IS ZERO
    r->GsErr = 0;                    // glideslope error indication
    r->DstTD = 0;                    // distance to touchdown point
    ReqDes = 0;                      // required angle of descent
  return(ReqDes);                    // return required angle of descent

When the aircraft is only a few metres above the runway, it ceases to descend at the normal fixed glideslope angle, gradually reducing its angle of descent to zero in order to make a smooth landing. This is called 'flair-out'.

Air navigation functions: the ILS glide-slope flair-out geometry.

The function ReqDes() performs this flair-out by shifting the touchdown point a quarter of the way down the runway and changing the glideslope angle to zero once the aircraft has gone below the critical height 'FlairHeight'. The damping effect of the way GsErr is computed causes the aircraft to make a smooth curved intercept with the surface of the runway.

Vertical Steering

The next task is to make the aircraft's actual angle of descent, Air.Des, the same as the required angle of descent, ReqDes:

Air navigation functions: the ILS glide-slope vertical steering geometry.

Assuming automatic trim etc., the angle of descent is mainly a function of the airc­raft's pitch and air speed. For the most part, airspeed would not be used to actually control the angle of descent. To control the angle of descent, we are therefore con­cerned only with changing pitch by a small amount 'dPitch' to counter the 'error' in angle of descent, DesErr = ReqDes - Air.Des. We shall make the amount by which we change the pitch proportional to DesErr:

dPitch = K1 * ( ReqDes - Air.Des );

Because the aircraft is a large finite object with physical limitations, we must im­pose an upper limit, RopLim, on the rate at which we can safely change the pitch. RopLim is the limit for the rate of change of pitch and is in radians per second. The limit to which we can change pitch during the elapsed time between program pas­ses is therefore RopLim * et radians. Therefore in response to a given angle of des­cent error, we update the aircraft's pitch as follows to bring the aircraft smoothly on to its correct descent path:

Air.Pitch += LimAng(K1 * BipAng(ReqDes - Air.Des), RopLim * et);

But there must be an upper limit, PitchLim, to the actual pitch itself. We must there­fore split the above statement into two as follows:

dPitch = LimAng(K1 * BipAng(ReqDes - Air.Des), RopLim * et);
Air.Pitch = LimAng(BipAng(Air.Pitch + dPitch), MaxPitch);

The change in angle of descent is proportional to the change in pitch. We can there­fore return a change in angle of descent as PitchDes * dPitch. The complete func­tion for determining the change in angle of descent effected by the airframe and flight control systems in response to a given angle of descent error is:

double GetdDes(double et, double DesErr) {
  #define PitchDesErr .03  // change in pitch per unit angle of descent error
  #define RopLim .01       // rate of change of pitch limit in radians/second
  #define MaxPitch .5      // maximum permitted pitch in radians
  #define DesPitch 1.00    // change in Air.Des per unit change in Air.Pitch

  dPitch = LimAng(K1 * BipAng(ReqDes(t, r) - Air.Des), RopLim * et);
  Air.Pitch = LimAng(BipAng(Air.Pitch + dPitch), MaxPitch);
  return(DesPitch * dPitch);

This function is our 'dummy' standing in place of a complete airframe simulation which would compute the change in angle of descent from pitch, trim, thrust, and the many other variables affecting the result in a real aircraft.

The whole vertical steering task is managed by the following function which, from the required angle of descent and the actual angle of descent, updates the air­craft's current height and angle of descent:

void VertSteer(struct TxData *t, struct RxData, *r, double et) {

  /*Compute angle of descent error and store it
    also as a non-linear instrument deviation: */

  r->DesErr = GetDev(double DesErr = BipAng(ReqDes(t, r) - Air.Des));

  /*Compute the required incremental change in the angle of descent 
    since the previous pass, then update the aircraft's angle of 
    descent and hence its height since previous pass. Note that Air.Spd 
    is the aircraft's horizontal ground track speed. Hence tan(). */

  Air.Ht -= Air.Spd * et * tan(Air.Des = RatAng(Air.Des + GetdDes(et, DesErr)));

Software Integration

Finally, to bring all these radio environment and navigation control func­tions in this document all together, they are called from the following overall navigation function which should be called regularly every 200 mil­liseconds.

As an exercise, this function has been headed as a timer call-back function to be called by a designated system timer within an operating system environment.

WORD FAR PASCAL Nav(  // called every 200 ms
  HWND hWnd,          // identifies the Nav window
  WORD wMsg,          // timer message resulting in this call
  int nIDEvent,       // id number of system timer which made the call
  DWORD dwTimer       // current system time
) {
  StnPoll();          // check rough distance of stations
  RxInput();          // scan receiver tuner inputs etc.
  TxScan();           // get station signal strengths

  struct RxData *r = *(*(Rx + 4) + Air.Sw); /* The NAV Rx which is currently
                                               switched to the NAV computer. */

  struct TxData *t = r->t ;       // dominant station currently being received
  double et = ElapsedTime();      // elapsed time since last pass
  if((int Sig = r->Sig) > 0)      // if in range of nav station
    HorzSteer(t, r, et);          // do horizontal (roll) steering
  if(((int st = t->StnType) == 4  // If the station is an ILS
  || st == 5)                     // or ILS/DME
  && (Sig > 85))                  // and it is within glideslope range
    VertSteer(t, r, et);          // do vertical (pitch) steering

For this to work you will have to set up the timer in the WM_CREATE case in the application's MainWndProc() as follows:

Air navigation functions: the navigation system's 200ms iteration timer.

'C' Functions

The following schematic shows the relationships between the 'C' functions which have been discussed in these research project notes.

Air navigation functions: The relationships between all the navigation C-functions.

Air navigation functions: The relationships between all the radio C-functions.

Circuit Breaker Logic

Within the functions discussed in these notes, many of the computations have been made conditional upon the states of various circuit breakers which control the power supplies to the various elements of the aircraft's on-board equipment. The positioning of these circuit breakers is indicated in the diagram below:

Air navigation functions: the circuit breaker logic of the navigation and radio aids systems.

© 1997 Robert John Morton