rm3100/diagnose-comms.py
Jeremy Karst 3d6c7251e9 Add exact-interval sampling, chip-grid timing, and analysis tooling
Sample every measurement exactly once, flagging losses in a warning
column rather than dropping them, so sample_index stays a valid
chip-time grid coordinate. Calibrate the true period before recording;
abort if no loss-free stretch exists.

Captures now store only raw counts plus a config header, with tesla and
both time bases reconstructed on load. Cycle count becomes the rate knob
(TMRC has no effect once it governs), raising the ceiling ~4x via
repeated-START reads and a shorter GIL switch interval.
2026-08-23 18:16:43 -04:00

138 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Hardware diagnostics for the RM3100 / CH347 link.
Walks the signal chain from USB inwards and stops at the first thing that is
broken, so a failure points at a specific wire rather than "it doesn't work":
adapter -> I2C bus -> sensor identity -> registers -> analog section
Usage: ./.venv/bin/python diagnose-comms.py
"""
import sys
import time
import ch347
import rm3100
# BIST with STE=1, BW=11 (4 sleep oscillation cycles), BP=11 (4 LR periods):
# the most forgiving self-test settings (manual Tables 5-6, 5-7).
BIST_RUN = 0x8F
# Matches the CH347I2C default; named here only so the report can state it.
BUS_KHZ = 750
def check(label, ok, detail=""):
print(f" [{'OK ' if ok else 'FAIL'}] {label}" + (f" -- {detail}" if detail else ""))
return ok
def main():
print("1. USB adapter")
try:
bus = ch347.CH347I2C()
except IOError as exc:
check("open CH347", False, str(exc))
print("\n-> Run ./setup.sh, or check the adapter is plugged in and in Mode 1.")
return 1
check("open CH347 and set I2C speed", True,
f"interface {ch347.INTERFACE} claimed, {BUS_KHZ} kHz")
with bus:
print("\n2. I2C bus")
found = bus.scan()
if not check("devices respond", bool(found),
", ".join(f"0x{a:02x}" for a in found) if found else "bus is silent"):
print("\n-> Nothing ACKs. In order of likelihood:")
print(" - I2CEN not tied HIGH, so the chip is in SPI mode (most common)")
print(" - SDA/SCL swapped")
print(" - missing pull-up resistors on SDA and SCL")
print(" - module not powered")
return 1
candidates = [a for a in found if a in rm3100.RM3100.ADDRESSES]
if not check("an address in the RM3100 range 0x20-0x23", bool(candidates),
", ".join(f"0x{a:02x}" for a in candidates) if candidates
else "responders are not RM3100 addresses"):
return 1
sensor = rm3100.RM3100(bus, candidates[0])
print(f"\n3. Sensor identity (at 0x{sensor.address:02x})")
revid = sensor.revid()
check("REVID", revid == rm3100.EXPECTED_REVID,
f"0x{revid:02x}" + ("" if revid == rm3100.EXPECTED_REVID
else f", expected 0x{rm3100.EXPECTED_REVID:02x}"))
print("\n4. Register read/write")
sensor.stop_cmm()
time.sleep(0.05)
original = sensor.get_cycle_counts()
sensor.set_cycle_counts(100)
readback = sensor.get_cycle_counts()
ok = readback == (100, 100, 100)
check("cycle counts write then read back", ok, f"wrote 100, read {readback}")
sensor.set_cycle_counts(original[0] or rm3100.DEFAULT_CYCLE_COUNT)
if not ok:
print("\n-> Registers are unreliable. Try a lower bus speed or shorter wires.")
return 1
print("\n5. Analog section (BIST -- drives the sensor coils)")
sensor.write_reg(rm3100.REG_BIST, [BIST_RUN])
sensor.write_reg(rm3100.REG_POLL, [rm3100.POLL_ALL_AXES])
time.sleep(0.5)
bist = sensor.read_reg(rm3100.REG_BIST)[0][0]
sensor.write_reg(rm3100.REG_BIST, [0x00])
axes = {"X": (bist >> 4) & 1, "Y": (bist >> 5) & 1, "Z": (bist >> 6) & 1}
all_ok = all(axes.values())
check("LR oscillators", all_ok,
f"BIST=0x{bist:02x} " + " ".join(f"{k}OK={v}" for k, v in axes.items()))
if not all_ok:
print("\n-> The coils are not oscillating, though I2C is fine. Check:")
if not any(axes.values()):
print(" - AVDD/AVSS not connected (most common when ALL axes fail).")
print(" Registers run on DVDD alone, so I2C works while the analog")
print(" section is dead. Tie AVDD to the same 3V3 rail as DVDD")
print(" (they must stay within 0.1 V), and AVSS to ground.")
print(" - REXT timing resistor (33k) missing")
else:
print(f" - coil wiring for {', '.join(k for k, v in axes.items() if not v)}")
return 1
print("\n6. Live measurement")
sensor.configure()
# BIST zeroes the measurement registers and leaves DRDY set. With
# DRC1=1 only a results read clears it, so discard one -- otherwise
# wait_for_data() returns instantly on the stale flag and reports zeros.
sensor.read_raw()
sensor.set_rate(0x96)
sensor.start_cmm()
try:
ready = sensor.wait_for_data(timeout=2.0)
if not check("DRDY asserts", ready, "" if ready else "no data within 2 s"):
return 1
counts, tesla = sensor.read_measurements()
magnitude = sum(v * v for v in tesla) ** 0.5
nt = [v * rm3100.NT_PER_TESLA for v in tesla]
plausible = check(
"field magnitude is plausible", 25e-6 <= magnitude <= 65e-6,
f"{magnitude * rm3100.NT_PER_TESLA:,.0f} nT "
"(Earth's field is 25,000-65,000)")
print(f" raw {counts}")
print(f" X {nt[0]:+,.0f} Y {nt[1]:+,.0f} Z {nt[2]:+,.0f} nT")
if not plausible:
print("\n-> Communication works but the numbers are wrong. A reading of\n"
" exactly zero means the results registers were read without a\n"
" fresh measurement behind them; anything else suggests the gain\n"
" or the int24 decode.")
return 1
finally:
sensor.stop_cmm()
print("\nAll checks passed -- the link is fully working.")
return 0
if __name__ == "__main__":
sys.exit(main())