365 lines
16 KiB
Markdown
365 lines
16 KiB
Markdown
|
|
# RM3100 logging over a CH347 USB-I2C adapter
|
|||
|
|
|
|||
|
|
Logs a PNI RM3100 geomagnetic sensor connected to the I2C pins of a Waveshare
|
|||
|
|
USB to UART/I2C/SPI/JTAG adapter (CH347, USB `1a86:55db`).
|
|||
|
|
|
|||
|
|
Reference: *RM3100 & RM2100 Sensor Suite User Manual*, Doc 1017252 R07
|
|||
|
|
(`PNI Sensor - RM3100-Sensor-Suite-User-Manual-R07-1.pdf` in this directory).
|
|||
|
|
Section numbers below refer to it.
|
|||
|
|
|
|||
|
|
## Hardware
|
|||
|
|
|
|||
|
|
Waveshare adapter in **Mode 1** (UART1 + I2C + SPI), voltage selector at **3V3**.
|
|||
|
|
The RM3100 is on a breakout board.
|
|||
|
|
|
|||
|
|
> 3V3 is required, though 3.0v would be ideal for the analog side.
|
|||
|
|
|
|||
|
|
### Wiring
|
|||
|
|
|
|||
|
|
All connections below are **confirmed working** — this exact wiring produces
|
|||
|
|
valid data.
|
|||
|
|
|
|||
|
|
| Adapter | RM3100 | Notes |
|
|||
|
|
|---|---|---|
|
|||
|
|
| I2C VCC (3V3) | DVDD | digital supply |
|
|||
|
|
| I2C VCC (3V3) | VDD (= AVDD) | analog supply — **required for measurements** |
|
|||
|
|
| I2C VCC (3V3) | I2CEN | **required to select I2C over SPI** |
|
|||
|
|
| I2C GND | DVSS | digital ground |
|
|||
|
|
| I2C SCL | SCK/SCL | pin 27, shared with SPI SCLK |
|
|||
|
|
| I2C SDA | SI/SDA | pin 1, shared with SPI MOSI |
|
|||
|
|
|
|||
|
|
All three 3V3 connections share the adapter's single I2C VCC pin. Current draw is
|
|||
|
|
negligible — ~260 µA per axis at 24 Hz (Table 3-1).
|
|||
|
|
|
|||
|
|
The two supply pins and I2CEN are the non-obvious ones; both are explained below,
|
|||
|
|
because omitting either produces a failure that is easy to misdiagnose.
|
|||
|
|
|
|||
|
|
#### I2CEN (pin 22) must be tied HIGH
|
|||
|
|
|
|||
|
|
`HIGH = I2C, LOW = SPI` (§4.3.1). The RM3100's SDA/SCL pins are shared with the
|
|||
|
|
SPI MOSI/SCLK pins, and I2CEN is what selects between the two interfaces. Left
|
|||
|
|
floating or low, the chip stays in SPI mode and **never ACKs its I2C address** —
|
|||
|
|
the bus scan finds nothing at all, even though the adapter is perfectly healthy.
|
|||
|
|
|
|||
|
|
At DVDD = 3.3 V it needs at least `VIH = 0.7 × DVDD` = 2.31 V, so tie it
|
|||
|
|
directly to 3V3.
|
|||
|
|
|
|||
|
|
#### AVDD/AVSS (pins 4, 5) must be powered to take measurements
|
|||
|
|
|
|||
|
|
§4.3.1: *"AVDD can be turned off when not making a measurement to conserve
|
|||
|
|
power, since all other operations are supported with DVDD."*
|
|||
|
|
|
|||
|
|
So with only DVDD connected, the part is **half alive** in a way that is easy to
|
|||
|
|
misread as working: the I2C address responds, REVID reads back `0x22`, and
|
|||
|
|
registers write and read back correctly — but the analog section that drives the
|
|||
|
|
sensor coils is dead, so DRDY never asserts and the measurement registers stay
|
|||
|
|
at zero. `BIST` reports `XOK=YOK=ZOK=0` (see Diagnostics below), which is the
|
|||
|
|
unambiguous tell.
|
|||
|
|
|
|||
|
|
**Both rails are needed — AVDD is not a substitute for DVDD.** Two constraints
|
|||
|
|
(§4.3.1, Table 3-5):
|
|||
|
|
- DVDD must come up **before or at the same time as** AVDD, never after. Moving
|
|||
|
|
the supply from DVDD to AVDD instead of adding it violates this.
|
|||
|
|
- AVDD must stay within **0.1 V** of DVDD while on — sharing one 3V3 rail
|
|||
|
|
satisfies this for free.
|
|||
|
|
|
|||
|
|
#### Power quality
|
|||
|
|
|
|||
|
|
The manual specifies very little here, but what it does specify is firm.
|
|||
|
|
|
|||
|
|
| Parameter | Limit | Source |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Ripple on AVDD or DVDD | **50 mV peak-to-peak** | Table 3-5, `VDD_ripple` |
|
|||
|
|
| DVDD − AVDD while running | **±0.1 V** | Table 3-5, `ΔVDD_OP` |
|
|||
|
|
| Supply range | 2.0–3.6 V (typ 3.0) | Table 3-5 |
|
|||
|
|
| Absolute maximum | 3.7 V | Table 3-4 |
|
|||
|
|
|
|||
|
|
The ripple limit carries **no frequency qualifier** — it is stated flat, so
|
|||
|
|
switching noise and mains hum are not distinguished.
|
|||
|
|
|
|||
|
|
Reference decoupling (Figures 4-1 and 4-2) is a **10 µF bulk capacitor in
|
|||
|
|
parallel with 0.1 µF ceramic**, rail to ground. One rail feeds AVDD, both DVDD
|
|||
|
|
pins, and I2CEN. That is the entire filtering specification — no ferrite, no
|
|||
|
|
split analog/digital rails, no LDO requirement. §4.2.3 adds a placement rule:
|
|||
|
|
*"Keep capacitors, especially tantalum capacitors, far away from the sensor
|
|||
|
|
coils"* — a magnetic concern (ferromagnetic packaging), not an electrical one.
|
|||
|
|
|
|||
|
|
**The manual gives no transfer function from ripple to field error** — no
|
|||
|
|
µT-per-mV, no PSRR, no ripple-vs-noise curve. Only the 50 mV limit.
|
|||
|
|
|
|||
|
|
Slow drift should matter less than fast ripple. §4.1 describes the output as
|
|||
|
|
*"the difference in the time to complete the measurement for each bias"* — a
|
|||
|
|
differential measurement across forward and reverse coil bias — and §2 claims
|
|||
|
|
measurements are *"stable over temperature and inherently free from offset
|
|||
|
|
drift."* Anything common to both half-measurements largely cancels, so supply
|
|||
|
|
variation slower than one measurement cycle is rejected far better than noise
|
|||
|
|
near the ~180 kHz LR oscillation. (That last step is inference from the
|
|||
|
|
described architecture, not an explicit claim in the manual.)
|
|||
|
|
|
|||
|
|
**This rig runs at 3.3 V, but every number in Table 3-1 is quoted at 3.0 V.**
|
|||
|
|
Footnote 1: *"Other bias resistors, external timing resistors and operating
|
|||
|
|
voltages may be used, but performance will differ from the values listed."*
|
|||
|
|
3.3 V is comfortably in range, but the 75 LSB/µT gain this driver uses is
|
|||
|
|
strictly a 3.0 V figure — a candidate scale-factor error the manual does not
|
|||
|
|
quantify. Unverified here: whether the breakout carries local decoupling, and
|
|||
|
|
what the CH347's 3V3 rail ripple actually measures against the 50 mV limit.
|
|||
|
|
|
|||
|
|
#### SA0 / SA1 set the I2C address
|
|||
|
|
|
|||
|
|
The top 5 address bits are fixed at `0b01000`; SA0 (pin 3) and SA1 (pin 28) set
|
|||
|
|
the low two, giving 0x20–0x23 (§4.5). On this breakout both are strapped high,
|
|||
|
|
so the sensor answers at **0x23**. These pins are shared with SPI SSN and MISO,
|
|||
|
|
so a breakout may label them `SSN/SA0` and `MISO/SA1`.
|
|||
|
|
|
|||
|
|
`logger.py` scans all four addresses, so no configuration is needed.
|
|||
|
|
|
|||
|
|
#### DRDY (pin 23) is not connected
|
|||
|
|
|
|||
|
|
Not required. The driver polls the STATUS register instead (§5.4.1), which the
|
|||
|
|
manual explicitly offers as an alternative.
|
|||
|
|
|
|||
|
|
## Setup
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
./setup.sh
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Idempotent. It installs a udev rule, creates `.venv`, and installs pyusb. It
|
|||
|
|
needs `sudo` for the udev rule only.
|
|||
|
|
|
|||
|
|
The rule is needed because the CH347's USB node defaults to `root:root 0664`.
|
|||
|
|
It grants the `plugdev` group access:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55db", GROUP="plugdev", MODE="0660"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Python 3.14 here is PEP 668 externally-managed, hence the venv rather than a
|
|||
|
|
system-wide `pip install`.
|
|||
|
|
|
|||
|
|
## Usage
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
./.venv/bin/python logger.py --duration 10 # log for 10 s
|
|||
|
|
./.venv/bin/python logger.py # log until Ctrl-C
|
|||
|
|
./.venv/bin/python logger.py --scan-only # bus scan only, for wiring checks
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Flag | Default | Meaning |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `--cycle-count` | 200 | gain/resolution vs. speed; 200 → 75 LSB/µT |
|
|||
|
|
| `--tmrc` | 0x96 | continuous-mode rate; 0x92 (~600 Hz) to 0x9D (~0.3 Hz) |
|
|||
|
|
| `--duration` | 0 | seconds, 0 = until Ctrl-C |
|
|||
|
|
| `--output` | timestamped | CSV path |
|
|||
|
|
| `--address` | autodetect | skip the scan |
|
|||
|
|
| `--scan-only` | — | scan and exit |
|
|||
|
|
|
|||
|
|
CSV columns: `timestamp_iso, elapsed_s, x_raw, y_raw, z_raw, x_uT, y_uT, z_uT,
|
|||
|
|
magnitude_uT`.
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
./.venv/bin/python plot.py capture_60s.csv # -> capture_60s.png
|
|||
|
|
./.venv/bin/python plot.py capture.csv --smooth 0 # raw only, no moving average
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Sample rate
|
|||
|
|
|
|||
|
|
**Cycle count, not TMRC, sets the ceiling.** Measured with `--bus-speed 400`:
|
|||
|
|
|
|||
|
|
| Cycle count | Measured (polled) | Table 3-1 predicts | Noise (Table 3-1) |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 200 | 152 Hz | 147 Hz | 15 nT |
|
|||
|
|
| 100 | 297 Hz | 283 Hz | 20 nT |
|
|||
|
|
| 50 | 538 Hz | 533 Hz | 30 nT |
|
|||
|
|
|
|||
|
|
**The manual contradicts itself here and Table 3-1 is the correct one.** The
|
|||
|
|
§5.2.1 note claims cycle count 200 allows a "~430 Hz" 3-axis update rate;
|
|||
|
|
Table 3-1 gives 440 Hz *single-axis* and says to divide by 3, i.e. ~147 Hz.
|
|||
|
|
Measurement backs Table 3-1 at all three cycle counts.
|
|||
|
|
|
|||
|
|
Bus speed matters less than expected: 400 kHz measured best (538 Hz), 100 kHz
|
|||
|
|
gave 444 Hz, and 750 kHz was slightly *worse* than 400 kHz at 515 Hz. Use 400.
|
|||
|
|
|
|||
|
|
Skipping the DRDY poll and free-running reads reaches 1100–1460 Hz, but 62–87%
|
|||
|
|
of those samples are duplicates, so it buys nothing real.
|
|||
|
|
|
|||
|
|
### Threading
|
|||
|
|
|
|||
|
|
The sampling loop does I2C and nothing else — it pushes raw counts onto a queue,
|
|||
|
|
and a writer thread does unit conversion, CSV formatting, flushing and the
|
|||
|
|
console display. Wall-clock timestamps are reconstructed in the writer from one
|
|||
|
|
start time plus each sample's monotonic offset, rather than calling
|
|||
|
|
`datetime.now()` per sample; that is both cheaper and immune to an NTP step
|
|||
|
|
mid-capture.
|
|||
|
|
|
|||
|
|
This is worth roughly **28%**: the same 60 s capture yielded 22,558 samples
|
|||
|
|
(376 Hz) with inline writing and 28,864 (481 Hz) with the writer split off.
|
|||
|
|
The queue is bounded, and any drop is counted and reported rather than
|
|||
|
|
silently losing data.
|
|||
|
|
|
|||
|
|
## Files
|
|||
|
|
|
|||
|
|
| File | Role |
|
|||
|
|
|---|---|
|
|||
|
|
| `setup.sh` | udev rule, venv, pyusb |
|
|||
|
|
| `ch347.py` | CH347 I2C transport — USB only, knows nothing about the sensor |
|
|||
|
|
| `rm3100.py` | RM3100 driver — registers and decoding, knows nothing about USB |
|
|||
|
|
| `logger.py` | CLI: scan → identify → configure → log |
|
|||
|
|
| `plot.py` | four-panel plot of a capture: X, Y, Z and the norm |
|
|||
|
|
| `diagnose.py` | walks USB → bus → identity → registers → BIST → live read, stopping at the first failure |
|
|||
|
|
|
|||
|
|
The adapter/sensor split is deliberate: `ch347.py` is a general I2C master usable
|
|||
|
|
with any device, and `rm3100.py` needs only a bus object exposing
|
|||
|
|
`write(addr, data)` and `read(addr, count)`.
|
|||
|
|
|
|||
|
|
## Implementation notes
|
|||
|
|
|
|||
|
|
Things that cost time to work out, recorded so they don't have to be again.
|
|||
|
|
|
|||
|
|
### CH347 access
|
|||
|
|
|
|||
|
|
The adapter presents three USB interfaces. Interfaces 0 and 1 are CDC-ACM and
|
|||
|
|
the kernel binds them as `/dev/ttyACM0` (the UART). **Interface 2** is
|
|||
|
|
vendor-class and carries I2C/SPI/JTAG with no kernel driver bound, so libusb can
|
|||
|
|
claim it directly with nothing to detach.
|
|||
|
|
|
|||
|
|
`ch347.py` deliberately never calls `set_configuration()` — the device is already
|
|||
|
|
configured, and re-setting it would disturb the CDC-ACM interfaces driving the
|
|||
|
|
UART.
|
|||
|
|
|
|||
|
|
There is no usable off-the-shelf option: no CH347 kernel driver exists (the
|
|||
|
|
in-tree `spi-ch341` is for `1a86:5512`, a different chip); the PyPI `ch347`
|
|||
|
|
package wraps a Windows DLL; and `ch347api` supports only HID mode (`55dc`), not
|
|||
|
|
this vendor-bulk mode 1. The framing in `ch347.py` follows the
|
|||
|
|
`aystarik/ch347-i2c-spi-gpio` Linux driver, whose id-table entry
|
|||
|
|
`USB_DEVICE_INTERFACE_NUMBER(0x1a86, 0x55db, 0x02)` matches this device exactly.
|
|||
|
|
|
|||
|
|
Wire format — bulk OUT `0x06`, bulk IN `0x86`, max 63 bytes per transfer:
|
|||
|
|
|
|||
|
|
| Purpose | Bytes out | Bytes back |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Set speed | `AA 6<speed> 00` | 0 |
|
|||
|
|
| Write n | `AA 74 (80\|n+1) (addr<<1) <data> 75 00` | n+1 |
|
|||
|
|
| Read n | `AA 74 81 ((addr<<1)\|1) [C0\|(n-1)] C0 75 00` | n+1 |
|
|||
|
|
| Probe | `AA 74 81 (addr<<1) 75 00` | 1 |
|
|||
|
|
|
|||
|
|
**Every returned byte must be `1`** — that is the per-byte ACK. On a read, byte 0
|
|||
|
|
is the address ACK and the rest is payload.
|
|||
|
|
|
|||
|
|
### RM3100 quirks
|
|||
|
|
|
|||
|
|
- **Register reads use the plain address, not `|0x80`.** §5 describes the SPI
|
|||
|
|
convention of adding 0x80, but the I2C diagram in §5.8.4 writes `0x24`
|
|||
|
|
literally. Only 7 bits are decoded, so both work; the plain form is used here.
|
|||
|
|
- **A read is STOP-then-START, not a repeated START** (§4.5.2, §5.8.4), which is
|
|||
|
|
exactly what two separate CH347 transactions produce. No special handling.
|
|||
|
|
- **CMM = 0x79** for all three axes. Table 5-1 describes bit 3 as reserved-zero,
|
|||
|
|
but the manual's own examples (§5.7.2, §5.8.3) set it. The examples win.
|
|||
|
|
- **HSHAKE is set to 0x0A** (`DRC0=0`, `DRC1=1`) during init. The 0x1B default has
|
|||
|
|
`DRC0=1`, meaning *any* register write clears DRDY — including the pointer
|
|||
|
|
write that reading STATUS itself requires, so polling could never observe DRDY
|
|||
|
|
set. With `DRC0=0`, DRDY clears only on a results read.
|
|||
|
|
- **Gain** is `0.3671 × cycle_count + 1.5` LSB/µT, a linear fit to Table 3-1
|
|||
|
|
(50→20, 100→38, 200→75; reproduces all three within a count).
|
|||
|
|
- Measurements are three **24-bit big-endian two's-complement** values, read as
|
|||
|
|
9 bytes from 0x24 using the sensor's register auto-increment.
|
|||
|
|
|
|||
|
|
## Diagnostics
|
|||
|
|
|
|||
|
|
Symptoms map cleanly onto causes, so work down this list.
|
|||
|
|
|
|||
|
|
| Symptom | Cause |
|
|||
|
|
|---|---|
|
|||
|
|
| `Cannot claim CH347 interface 2: Access denied` | udev rule missing — run `./setup.sh` |
|
|||
|
|
| `No CH347 adapter found` | not plugged in, or not in Mode 1 |
|
|||
|
|
| Adapter opens, `--scan-only` finds nothing | **I2CEN not tied high** (most likely), or SDA/SCL swapped, or no bus pull-ups |
|
|||
|
|
| Found at 0x23, REVID `0x22`, registers fine, but DRDY never sets and results are all zero | **AVDD/VDD not powered** — confirm with BIST; both of these were hit during bring-up |
|
|||
|
|
| Cycle-count read-back mismatch | bus integrity — try a lower speed |
|
|||
|
|
|
|||
|
|
**BIST is the definitive test for the analog side** (§5.6.1). Write `0x8F` to
|
|||
|
|
BIST (STE=1, max timeout and periods), write `0x70` to POLL, wait, then read
|
|||
|
|
BIST back: bits 4/5/6 are XOK/YOK/ZOK, and `1` means that axis's LR oscillator
|
|||
|
|
ran. All zeros means the coils are not oscillating, which points at AVDD or the
|
|||
|
|
REXT timing resistor rather than anything on the I2C side.
|
|||
|
|
|
|||
|
|
A healthy total field magnitude is roughly **25–65 µT** (Earth's field). Near
|
|||
|
|
zero, railed, or wildly out of range means the decode or gain is wrong rather
|
|||
|
|
than merely "data arrived".
|
|||
|
|
|
|||
|
|
## Observed performance
|
|||
|
|
|
|||
|
|
First working capture, 10 s at TMRC 0x96, cycle count 200, sensor stationary on
|
|||
|
|
a desk next to a PC:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
349 samples over 10.02 s -> 34.8 Hz
|
|||
|
|
X: mean +6.473 uT sd 177 nT
|
|||
|
|
Y: mean +50.914 uT sd 106 nT
|
|||
|
|
Z: mean +18.944 uT sd 396 nT
|
|||
|
|
|B|: mean 54.709 uT sd 245 nT
|
|||
|
|
sample interval: 28.78 ms, sd 1.52 ms
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- **Rate.** 34.8 Hz against a nominal ~37 Hz is in spec: §5.2.1 quotes roughly
|
|||
|
|
7% one-standard-deviation tolerance on the update rate.
|
|||
|
|
- **Magnitude.** 54.7 µT sits in the expected 25–65 µT band for Earth's field,
|
|||
|
|
which is the real confirmation that gain and the int24 decode are right.
|
|||
|
|
- **Noise** is well above the 15 nT that Table 3-1 quotes for cycle count 200,
|
|||
|
|
and is not a driver problem. Most likely proximity to a PC and the USB
|
|||
|
|
adapter; a secondary candidate is ripple on the CH347's 3V3 rail against the
|
|||
|
|
50 mV limit (see Power quality above), which has not been measured. Expect far
|
|||
|
|
better readings away from mains wiring and switching supplies.
|
|||
|
|
- **Drift.** All three axes drifted monotonically down over the 10 s (Z most, by
|
|||
|
|
about 1.3 µT), so |B| fell from 55.5 to 54.3. Consistent with thermal settling
|
|||
|
|
after AVDD is first powered — coil DC resistance moves 0.4 %/°C (Table 3-3).
|
|||
|
|
Worth allowing a warm-up period before trusting absolute values.
|
|||
|
|
|
|||
|
|
None of this is calibrated: the figures are raw sensor output with no hard- or
|
|||
|
|
soft-iron correction, so the individual axis values reflect local distortion as
|
|||
|
|
much as Earth's field.
|
|||
|
|
|
|||
|
|
### 60 s capture at maximum rate
|
|||
|
|
|
|||
|
|
`capture_60s.csv` / `capture_60s.png` — 28,864 samples, 481 Hz, cycle count 50,
|
|||
|
|
400 kHz bus. The sensor was nudged by hand at t ≈ 41 s, so statistics are split
|
|||
|
|
around that to keep the noise figures honest:
|
|||
|
|
|
|||
|
|
| Axis | sd, 0–40 s | sd, 47–60 s | step across the event |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| X | 84 nT | 85 nT | +280 nT |
|
|||
|
|
| Y | 388 nT | 376 nT | +217 nT |
|
|||
|
|
| Z | 187 nT | 198 nT | +232 nT |
|
|||
|
|
| \|B\| | 369 nT | 358 nT | +313 nT |
|
|||
|
|
|
|||
|
|
Three things worth noting:
|
|||
|
|
|
|||
|
|
- **The t ≈ 41 s step was the sensor being physically moved**, confirmed at the
|
|||
|
|
time — not an electrical artefact. Note that a norm shift does *not* by itself
|
|||
|
|
imply an external source changed: |B| is preserved under **rotation** in a
|
|||
|
|
uniform field, but a **translation** through a field gradient samples a
|
|||
|
|
different local field and changes the magnitude. Near a PC the gradients are
|
|||
|
|
steep (§4.2.2: field falls off as 1/distance³), so a few centimetres is ample
|
|||
|
|
to produce the +313 nT seen here. Treat |B| changes as "not a pure rotation",
|
|||
|
|
nothing more.
|
|||
|
|
- **Noise is strongly axis-dependent** — Y is 4.6× X (388 vs 84 nT), and stable
|
|||
|
|
across the event, so it is not a consequence of the disturbance. The coils are
|
|||
|
|
nominally identical, so this asymmetry points at orientation relative to a
|
|||
|
|
local noise source rather than a sensor fault. X at 84 nT is within ~3× of the
|
|||
|
|
30 nT Table 3-1 quotes for cycle count 50; Y is ~13×.
|
|||
|
|
- **Sample timing is jittery**: intervals average 2.08 ms (481 Hz) with sd
|
|||
|
|
1.09 ms and a 19.6 ms worst case. Fine for logging, but the irregular spacing
|
|||
|
|
makes this data unsuitable for spectral analysis without resampling.
|
|||
|
|
|
|||
|
|
## Status
|
|||
|
|
|
|||
|
|
- [x] udev rule and venv setup
|
|||
|
|
- [x] CH347 vendor protocol — verified byte-for-byte against the kernel driver
|
|||
|
|
- [x] I2C bus scan — sensor found at 0x23
|
|||
|
|
- [x] Sensor identified — REVID `0x22`
|
|||
|
|
- [x] Register write/read verified — cycle counts written and read back
|
|||
|
|
- [x] Analog section verified — BIST reports `XOK=YOK=ZOK=1`
|
|||
|
|
- [x] **Live measurements logging to CSV at ~35 Hz**
|
|||
|
|
|
|||
|
|
Initial communication is complete. A proper Python API is planned; these modules
|
|||
|
|
are the initial-communication milestone, deliberately kept simple.
|