Skip to content

SWI 0x09 — ArcTan

  • Entry: 0x00000474 (ARM)
  • Status: verified (hardware-checked 2026-07-08: cycle counts + full-range 65536-input dump matching the documented algorithm bit-for-bit)

Summary

Fixed-point arctangent of a single Q1.14 value. Evaluates an 8-term odd polynomial approximation of atan(x) using Horner's scheme with fixed-point (Q14) multiplies. Accurate to a fraction of a degree only for |x| <= 1.0; it diverges badly outside that range (see Edge cases). ArcTan2 (SWI 0x0A) exists precisely to keep the argument within the valid range.

Parameters

Reg In Meaning
r0 s16/Q1.14 Tangent value: 1 sign bit, 1 integer bit, 14 fraction bits (0x4000 = 1.0)

Returns

Reg Out Meaning
r0 s Angle in units where 0x10000 = 360°, i.e. 0x4000 = 90°. For valid inputs the result lies in about -0x2000..0x2000 (±45°)

Clobbered registers

r1, r3 are scratch. Only r0 is meaningful on return.

Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0 = result; r1/r3 hold internal leftovers (0xFFFFFC00 / 0x9FB3 observed); r2 preserved. r2, r4–r12, r13, and CPSR (flags and mode) came back bit-identical on every tested path. r11/r12 are explained by the SWI dispatcher (it pushes {r11, r12, lr} — see 10_irq_boot_and_iwram.md) and CPSR by the SPSR restore on return; r2 and r4–r10 must be preserved or restored by the routine itself — where a static note above claims r2 is destroyed, the hardware disagrees at the caller level. Raw data: results/clobber_audit_pass2.csv.

Algorithm

Hardware-exact: with 32-bit wraparound on every multiply and arithmetic-shift truncation as written below, this algorithm reproduces the real BIOS output bit-for-bit for all 65536 inputs (full-range dump, 2026-07-08 — results/arctan_full_dump.csv). The 32-bit MUL wraparound matters: in the divergent region s*t overflows 32 bits, which is what produces the chaotic outputs there.

Let x be the Q1.14 input. First compute t = -(x*x >> 14) (a Q14 value equal to -x^2). Then evaluate a polynomial P(t) by Horner's method, seeding an accumulator and, at each step, doing acc = (acc * t) >> 14 followed by adding the next coefficient. Finally the result is (P(t) * x) >> 16.

Because t = -x^2, the alternating signs of the arctan Taylor-like series are produced automatically, making this an odd polynomial in x.

Extracted coefficients (raw immediate constants, in Horner order)

The add immediates are assembled from the split ARM immediates in the binary:

Step Constant (dec) Constant (hex)
seed 169 0x00A9
+ 912 0x0390
+ 2332 0x091C (0x900 + 0x1C)
+ 4022 0x0FB6 (0xF00 + 0xB6)
+ 5802 0x16AA (0x1600 + 0xAA)
+ 8321 0x2081 (0x2000 + 0x81)
+ 13905 0x3651 (0x3600 + 0x51)
+ 41721 0xA2F9 (0xA200 + 0xF9)

Equivalently, with s = acc starting at 169:

s = 169
s = (s*t >> 14) + 912
s = (s*t >> 14) + 2332
s = (s*t >> 14) + 4022
s = (s*t >> 14) + 5802
s = (s*t >> 14) + 8321
s = (s*t >> 14) + 13905
s = (s*t >> 14) + 41721
result = (s * x) >> 16
The last coefficient 41721 (0xA2F9) is the dominant linear term: it maps the Q14 slope of atan near 0 into the output's 0x10000 = 360° scale (0xA2F9 / 0x10000 * 360° ≈ 57.29°/rad, the radian-to-this-unit factor).

Edge cases & known bugs

  • Valid range |x| <= 1.0 (0x4000): near-perfect. Hardware dump: x=0.25 → 14.04° (true 14.04°), x=0.5 → 26.56° (true 26.57°), x=1.0 → exactly 45.000°. Max absolute error over the whole [-1,+1] range is 1.35 output units (0.0074°), at x=0x3FE8. The output is monotonic in this range except for 6 single-unit dips (e.g. at 0x3F0A, 0x3F14, and mirrored points; also f(0x4001) < f(0x4000)).
  • Known inaccuracy — |x| > 1.0 (full-range hardware dump, 2026-07-08): the polynomial was fitted only for |x| <= 1; beyond that it degrades, then the 32-bit overflow of s*t makes it chaotic. Max |error| per band:
Input band Max error At
1.00–1.25 0.12° x=1.25
1.25–1.50 6.95° x≈1.4995
1.50–1.75 153.5° x=1.75 (returns −93.27°)
1.75–2.00 242.77° x≈1.9873

Error stays below 1° up to x=1.374 (0x57EF) and below 10° up to x=1.527 (0x61B9). From x≈1.666 (0x6A9F) the output's sign flips 12 times across the rest of the positive range, swinging over the full ±0x8000 span (+32758 at x≈1.792, −32768 two inputs earlier). x=1.75 → −93.27° and the ~243° peak error predicted by static analysis are confirmed exactly. The error source is the polynomial's limited fit domain, compounded by the repeated asr #14 truncation after each multiply and, past x≈1.6, by 32-bit overflow of the Horner accumulator product. - Not an odd function: f(−x) = −f(x) − 1 for 32760 of the 32767 ± pairs. The asr truncations floor toward −∞, leaving a systematic 1-unit (0.0055°) negative bias — e.g. ArcTan(0x1000) = +0x9FB but ArcTan(−0x1000) = −0x9FC. Exactly 7 inputs are symmetric (f(−x) = −f(x)): 0x0F38, 0x2000 (x=0.5), 0x2A50, 0x4000 (x=1.0), 0x4B40, 0x6000 (x=1.5), 0x79F0 — so the headline values ArcTan(±1.0) = ±0x2000 happen to look odd-symmetric. - Because callers should keep |tan| <= 1, the practical primitive is ArcTan2, which always feeds ArcTan a ratio with magnitude ≤ 1.

Cycle count

Hardware-measured net CPU cycles (worker-ROM harness, TM0/TM1 cascade at F/1, 13-cycle baseline subtracted; identical across 3 runs — see 02_hardware_verification_checklist.md § Measurement setup).

Constant 100 cycles for every nonzero input tested (0x1000, 0x4000, 0x7FFF, -0x8000); 98 for input 0. Confirms the fixed Horner evaluation with no data-dependent branching.

Open questions (need hardware verification)

None. The full-range hardware dump (results/arctan_full_dump.csv, all 65536 inputs) matches the extracted algorithm bit-for-bit, which pins down the error curve, the divergence/sign-flip profile, and the rounding behavior exactly.

GBATEK cross-reference

GBATEK notes ArcTan "has a hidden inaccuracy" but does not give coefficients or the error curve. This document adds the eight extracted polynomial coefficients, the exact Q14 fixed-point evaluation scheme, the measured accuracy inside |x|<=1, and the concrete divergence (and its cause) outside that range.