Reference
How a phone works out which way it is pointing
A phone has no needle and nothing that swings. It has a magnetometer, an accelerometer and a gyroscope — three streams of numbers with no notion of north between them — and the heading is computed from all three. This is the arithmetic, the published equations it comes from, a calculator that runs them on your own sensor readings, and what the calibration status is actually telling you.
The three sensors and what they report
| Sensor | Android type | Measures | Units |
|---|---|---|---|
| Magnetometer | TYPE_MAGNETIC_FIELD | magnetic field on three axes | microtesla (µT) |
| Accelerometer | TYPE_ACCELEROMETER | acceleration on three axes, gravity included | m/s² |
| Gyroscope | TYPE_GYROSCOPE | rate of rotation about three axes | radians per second |
None of them knows where north is. The magnetometer gives a vector in the phone’s own axes; turning that into a bearing takes the other two. Android reports all three in the same axes: x to the right, y toward the top edge, z out of the screen. Lying face up on a table, the accelerometer reads +9.81 m/s² on z.
The easy case: phone held flat
Lay the phone on a table and the magnetometer’s x and y axes lie in the horizontal plane. The heading is then one arctangent of the two horizontal readings. In the axes used by the method below — x along the direction the phone points, y to its right — it is:
ψ = atan2( −By, Bx )
In Android’s own axes, where x is to the right and y is the pointing direction, the same heading is atan2(−Bx, By). That is the whole calculation, and it is why a phone compass appears trivial — until the phone is tilted, when the two readings are no longer horizontal and the arctangent is measuring something else.
The real case: tilt compensation
A widely used published method is NXP application note AN4248, Implementing a Tilt-Compensated eCompass using Accelerometer and Magnetometer Sensors. It uses the North-East-Down convention: the phone’s x-axis is the pointing direction, y points right, z points down. G is the accelerometer reading, B the magnetometer reading, and V the hard-iron offset vector described on the interference page.
Step one — roll and pitch from gravity. With the phone still, the accelerometer reads only gravity, and the direction of gravity fixes two of the three angles (AN4248 equations 13 and 15):
tan(φ) = Gy / Gz
tan(θ) = −Gx / ( Gy sinφ + Gz cosφ )
Step two — rotate the magnetic vector back to level. Knowing roll φ and pitch θ, the magnetic reading is de-rotated into the horizontal plane (AN4248 equation 19):
Bfx = (Bx−Vx) cosθ + (By−Vy) sinθ sinφ + (Bz−Vz) sinθ cosφ
Bfy = (By−Vy) cosφ − (Bz−Vz) sinφ
Step three — the heading. Those two levelled components give the yaw angle relative to magnetic north (AN4248 equation 22):
ψ = atan2( −Bfy, Bfx )
Set roll and pitch to zero and this collapses back to the flat-phone formula — a useful check on any implementation. AN4248 computes roll and yaw with a two-argument arctangent over −180° to +180° and pitch with a one-argument arctangent restricted to −90° to +90°, so exactly one solution exists for any orientation.
AN4248’s axes and signs are not Android’s. Its G reads +1 g on its z-axis, which points down, when the board lies flat; Android’s accelerometer reads +9.81 on a z-axis that points up. Mapping one onto the other gives G = (−ay, −ax, az) and B = (my, mx, −mz), where a and m are Android’s accelerometer and magnetometer values.
Tilt-compensated heading calculator
Paste one accelerometer reading and one magnetometer reading taken at the same moment, and this runs AN4248 equations 13, 15, 19 and 22 on them. The defaults are a phone at Casper pointing 60° magnetic, nose up 10°, right side down 5°.
What it assumes. The phone is still, so the accelerometer is reading gravity alone; it warns when the magnitude says otherwise. The heading is that of the phone’s top edge, as Android’s azimuth is. Android’s TYPE_MAGNETIC_FIELD values already have the hard-iron offset removed, so leave V blank for them; fill it in only for uncalibrated readings. Soft iron is not modelled. The result is relative to magnetic north unless you add declination, which the declination calculator gives for any place and date. Tested against AN4248’s own forward model and against a port of Android’s getRotationMatrix() and getOrientation().
Why a small tilt error is a large heading error
The field is mostly vertical at mid and high latitudes, so a tilt error leaks the large vertical component into the small horizontal one. For a small tilt error ε the worst-case heading error is:
Δψ ≈ ε × (Z / H) = ε × tan(I)
At Casper, Wyoming the World Magnetic Model 2025 gives Z = 48,682 nT and H = 19,593 nT for 21 September 2026 — an inclination of 68.077° and a ratio of 2.49. One degree of tilt error becomes up to 2.5° of heading error. In Singapore, at an inclination of −12.752°, the same tilt error costs 0.23°. Wherever the dip is steeper than 45° a degree of tilt error costs more than a degree of heading, and that is the whole contiguous United States: even at Key West WMM2025 puts the dip at 52.9°.
AN4248 states the assumption plainly: a tilt-compensated eCompass gives erroneous readings under any linear acceleration, because the calculation assumes the accelerometer is reading gravity and nothing else. Walking, a vehicle, or a hand moving the phone each break that assumption while they last.
What the gyroscope is for
A gyroscope measures rate of rotation, not angle. Integrating rate over time gives angle, and integrating a constant error gives an error that grows without limit. The Bosch BMI270 — a consumer MEMS inertial measurement unit, which Bosch lists for wearables and AR/VR devices — specifies a typical zero-rate offset of ±0.5 degrees per second at 25 °C over its lifetime, with a further ±0.015 degrees per second per kelvin of temperature change, and output noise of 0.010 degrees per second per √Hz in normal mode (Bosch Sensortec BMI270 datasheet, BST-BMI270-DS000-08).
Integrate an uncorrected 0.5 °/s offset for one minute and the heading has drifted 30°. A ten-kelvin warm-up alone adds 0.15 °/s, which is 9° per minute. Android requires the gyroscope values an app receives to have an online bias calibration applied to remove drift, which shrinks these figures, but whatever bias remains still integrates without limit. A gyroscope on its own cannot hold a bearing.
What it can do is describe short-term motion with no reference to any external field. The two failure modes are opposite:
- The magnetometer and accelerometer give an absolute heading that does not drift, but it is noisy, wrong under linear acceleration, and badly wrong near steel.
- The gyroscope gives a smooth, immediate, interference-proof change in heading that drifts steadily away from the truth.
Sensor fusion is the name for combining them so each covers the other’s weakness. The simplest working form is a complementary filter — a weighted blend of the integrated gyroscope angle and the directly measured angle:
ψk = α (ψk−1 + ω Δt) + (1 − α) ψmag with α = τ / (τ + Δt)
The gyroscope supplies the fast changes, the magnetometer the slow truth, and τ is the crossover time constant deciding where one hands over to the other. That constant is a design choice, not a measured quantity: short and the output is jittery, long and it lags the phone. (Headings wrap at 360°, so a real implementation blends the difference between the two angles, not the raw numbers.) A Kalman filter does the same job with the weighting derived from each sensor’s noise statistics rather than fixed in advance.
Sampling rate
The AKM AK09918 magnetometer takes 7.2 ms typical for a single measurement and offers continuous modes at 10, 20, 50 and 100 Hz (AKM AK09918 datasheet, 016014242-E-00). At 50 Hz a new magnetic vector arrives every 20 ms. That sets how often the absolute reference is refreshed and how many samples are available to average down noise; between magnetometer samples, the fast response comes from the gyroscope.
What the calibration status means
Android reports an accuracy value alongside every sensor reading. The documented meanings are:
| Constant | Android’s description |
|---|---|
SENSOR_STATUS_UNRELIABLE | The values returned by this sensor cannot be trusted, calibration is needed or the environment doesn’t allow readings |
SENSOR_STATUS_ACCURACY_LOW | This sensor is reporting data with low accuracy, calibration with the environment is needed |
SENSOR_STATUS_ACCURACY_MEDIUM | This sensor is reporting data with an average level of accuracy, calibration with the environment may improve the readings |
SENSOR_STATUS_ACCURACY_HIGH | This sensor is reporting data with maximum accuracy |
What is being calibrated is the hard-iron offset vector V from the equations above and the soft-iron distortion: Android’s sensor specification requires online hard-iron calibration and factory or online soft-iron calibration of the magnetometer readings an app receives. Rotate a perfect magnetometer through every orientation and the tip of the measured vector traces a sphere centred on the origin, of radius equal to the local field strength. A hard-iron offset moves that sphere off the origin; a soft-iron distortion squashes it into an ellipsoid.
Calibration is therefore a fitting problem — collect readings over many orientations, fit the sphere or ellipsoid, take the centre as V. That is what the figure-eight gesture is for: covering enough of the sphere in a few seconds for the fit to be determined. Because the estimate is updated online, the accuracy value can change on its own as you move the phone.
One thing the status does not tell you: whether the field you are standing in is the Earth’s. A steel door frame’s field is fixed to the room, not to the phone, so turning the phone beside it traces the same kind of sphere Earth’s field does. The fit can converge and report high accuracy — its centre is still the phone’s own offset — while the field it sees on the sphere is Earth’s plus the door’s. Catching that needs an independent check against the expected field strength and dip angle, covered on the interference page along with what those checks can miss.
What Android hands the app
The platform will do the fusion for you. SensorManager.getRotationMatrix() takes an accelerometer reading and a magnetometer reading and returns a rotation matrix; getOrientation() turns that matrix into three angles in radians, where values[0] is the azimuth — the angle between the device’s y-axis and magnetic north, zero facing north, π/2 facing east, over the range −π to π. There is also TYPE_ROTATION_VECTOR, which fuses all three sensors, and TYPE_GEOMAGNETIC_ROTATION_VECTOR, which does the same job without the gyroscope, at lower power; Android’s documentation says it is noisier and works best outdoors.
Every one of those gives a heading relative to magnetic north. Converting to true north is a separate step requiring your position and the date, covered in magnetic declination and true north.
Bearing takes its attitude from Android’s rotation vector — the platform’s own fusion of accelerometer, gyroscope and magnetometer — requested at the game rate, a 20 ms interval, nominally 50 Hz. On phones without a gyroscope it falls back to the geomagnetic rotation vector, and failing that to getRotationMatrix() on a smoothed accelerometer reading. It adds declination from Android’s built-in geomagnetic model for true north, shows a bubble level so tilt is visible rather than inferred, shows the calibration status above as CAL 0/3 to 3/3 and asks for a figure-eight below 2, and warns when the measured field strength is more than 18% away from the model value. It works flat or upright, the compass runs offline, and its one banner ad is removable with a $2.99 one-time purchase.