The accepted algorithm for use in Marine AIS systems (specified in IEC61193-2) is the Rhumb Line algorithm. I have successfully implemented this on a target using Anthony Williams' fixed point maths library, which uses the CORDIC algorithm, and will I believe typically give a bout 5x performance improvement over software floating point.
However the library is C++ rather than C, which makes it easy to use due to extensive operator overloading, but is not perhaps what you are looking for. Worth considering using C++ compilation for your C code however, just for the benefit of this library. The problem with that of course is that Microchip's C31 compiler bizarrely does not support C++.
One caveat however is that the look-up table for the log()
function is too short by one value and needs an additional element at the end with value zero. This was confirmed by Anthony after I found it, but I do not believe that he has updated the download.
Either way, the answer is probably to use fixed point, and CORDIC.
To resolve to 1m of longitude or equatorial arc, you will need 8 digits of precision, so a single precision float will be insufficient, using double precision will slow things considerably. Checking MikroElectronica's C User Manual reveals that the compiler only supports single precision - float
, double
, and long double
are all 32-bit, so there is no way to achieve the accuracy you need using the built-in FP types in any case with this compiler.
If it is of any use, here is my Rhumb Line code using Anthony's library:
Header:
#if !defined cRhumbLine_INCLUDE
#define cRhumbLine_INCLUDE
#include "fixed.hpp"
//! Rhumb Line method for distance and bearing between two geodesic points
class cRhumbLine
{
public:
//! @brief Default constructor
//!
//! Defines a zero length line, bearing zero
cRhumbLine() : m_update_bearing(false), m_update_distance(false), m_distance(0), m_bearing(0) {}
//! @brief Constructor defining a line
//!
//! @param startLatDeg Start latitude in degrees, negative values are south of equator
//! @param startLonDeg Start longitude in degrees, negative values are west of meridian.
//! @param endLatDeg End latitude in degrees, negative values are south of equator
//! @param endLonDeg End longitude in degrees, negative values are west of meridian.
cRhumbLine( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg )
{
define( startLatDeg, startLonDeg, endLatDeg, endLonDeg ) ;
}
//! @brief Define a start/ent point
//!
//! @param startLatDeg Start latitude in degrees, negarive values are south of equator
//! @param startLonDeg Start longitude in degrees, negative values are west of meridian.
//! @param endLatDeg End latitude in degrees, negarive values are south of equator
//! @param endLonDeg End longitude in degrees, negative values are west of meridian.
void define( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg ) ;
//! @brief Get start-end distance in nautical miles
//! @return Start-end distance in nautical miles.
fixed distanceNm() { return distanceMetres() / ONE_NM_IN_METRE ; }
//! @brief Get start-end distance in metres.
//! @return Start-end distance in metres.
fixed distanceMetres() ;
//! @brief Get start-end bearing in degreed.
//! @return Start-end bearing in degreed (0 <= x < 360).
fixed bearingDeg() ;
private:
static const int ONE_NM_IN_METRE = 1852 ;
bool m_update_bearing ;
bool m_update_distance ;
fixed m_distance ;
fixed m_bearing ;
fixed m_delta_phi ;
fixed m_delta_lon ;
fixed m_delta_lat ;
fixed m_lat1_radians ;
} ;
#endif
Body:
#include "cRhumbLine.h"
void cRhumbLine::define( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg )
{
fixed lat1 = startLatDeg / 180 * fixed_pi ;
fixed lon1 = startLonDeg / 180 * fixed_pi ;
fixed lat2 = endLatDeg / 180 * fixed_pi ;
fixed lon2 = endLonDeg / 180 * fixed_pi ;
m_update_bearing = true ;
m_update_distance = true ;
m_delta_phi = log( tan( lat2 / 2 + fixed_quarter_pi ) / tan( lat1 / 2 + fixed_quarter_pi ) ) ;
m_delta_lat = lat1 - lat2 ;
m_delta_lon = lon1 - lon2 ;
m_lat1_radians = lat1 ;
// Deal with delta_lon > 180deg, take shorter route across meridian
if( m_delta_lon.abs() > fixed_pi )
{
m_delta_lon = m_delta_lon > 0 ? -(fixed_two_pi - m_delta_lon) : (fixed_two_pi + m_delta_lon) ;
}
}
fixed cRhumbLine::distanceMetres()
{
if( m_update_distance )
{
static const fixed mean_radius = 6371009 ; // in metres. Source: International Union of Geodesy and Geophysics (IUGG)
fixed q = m_delta_phi != 0 ? m_delta_lat / m_delta_phi : cos( m_lat1_radians ) ; // Deal with lines with constant latitude
m_distance = sqrt( ( sqr(m_delta_lat) + sqr(q) * sqr(m_delta_lon) ) ) * mean_radius ;
m_update_distance = false ;
}
return m_distance ;
}
fixed cRhumbLine::bearingDeg()
{
if( m_update_bearing )
{
static const fixed mean_radius = 6371009 ; // in metres. Source: International Union of Geodesy and Geophysics (IUGG)
m_bearing = atan2( m_delta_lon, m_delta_phi ) / fixed_pi * 180 ;
if( m_bearing == -180 )
{
m_bearing == 0 ;
}
else if( m_bearing < 0 )
{
m_bearing += 360 ;
}
m_update_bearing = false ;
}
return m_bearing ;
}