diff --git a/HX711.md b/HX711.md new file mode 100644 index 00000000..6ff9b24c --- /dev/null +++ b/HX711.md @@ -0,0 +1,156 @@ +# HX711S reliability design and implementation record + +This document records the Centauri Carbon load-cell acquisition design, the +evidence used to choose it, and the exact remaining validation work. It is +intended to be sufficient for a future maintainer to reproduce the analysis +without rediscovering the electrical protocol or reverse engineering the +stock firmware again. + +## Scope and evidence + +The affected path is the four-channel `hx711s` sensor on the bed MCU: + +``` +HX711 DOUT/SCK pairs -> src/sensor_hx711s.c -> sensor_bulk records + -> klippy/extras/load_cell/hx711s.py -> load_cell.py / SOS filter + -> src/load_cell_probe.c -> trsync homing trigger +``` + +The reference material is deliberately preserved in +`/home/paul/carbon/cc-firmware/stock/strain-gauge-hx711/`: + +* `HX711F_EN.pdf` — the HX711F data sheet used for the protocol limits; +* `firmware/upgrade_sg-1.6.9.bin` and `upgrade_sg-1.7.0.bin` — stock + Elegoo strain-gauge firmware; +* `r2/*.r2` — repeatable ARM Thumb r2ghidra scripts, with generated + pseudo-C beside them; and +* `COMPARISON.md` — version/hash and reverse-engineering notes. + +Useful external references: + +* [HX711F data sheet](https://cdn.sparkfun.com/assets/b/f/5/a/e/hx711F_EN.pdf) +* [Klipper HX711 discussion](https://klipper.discourse.group/t/hx711-support-load-cell-sensor/2057/8) +* [original upstream Klipper proposal](https://github.com/Klipper3d/klipper/pull/5492) +* [current upstream HX71x MCU code](https://github.com/Klipper3d/klipper/blob/7046bd00ef5c30dec6febc724f8d22967433c45c/src/sensor_hx71x.c) +* [Linux IIO HX711 driver](https://github.com/torvalds/linux/blob/248951ddc14de84de3910f9b13f51491a8cd91df/drivers/iio/adc/hx711.c) +* [Prusa HX717 implementation](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/e96ce2b92adcdafae19c132e76a81397b717e057/src/common/hx717.cpp) + +## What the hardware requires + +The data sheet makes the MCU, not Python, the only place that can decide if a +serial frame was acquired correctly: + +| Requirement | Consequence in this driver | +| --- | --- | +| DOUT low means a conversion is ready; SCK must remain low while waiting. | A pre-read high DOUT makes the whole lock-step frame invalid. | +| Read 24 data bits, then 1/2/3 gain-selection pulses. | Check every extra bit and record its failure. | +| SCK high/low is 0.2–50 us; high for more than 60 us powers down the chip. | Clock transitions are explicit; IRQs are masked only for the 1-us high phase. | +| Results are signed 24-bit two's complement. | Sign extension remains in MCU code and rail values are flagged. | +| After reset or a gain change, four conversions must settle (50 ms at 80 SPS, 400 ms at 10 SPS). | Reset is followed by five discarded frames (gain-select plus four settling conversions), then two clean qualification frames. | + +## Comparative audit + +| Area | Previous Cosmos behavior | Elegoo / upstream / Prusa lesson | Selected behavior | +| --- | --- | --- | --- | +| Frame error handling | MCU and host each used different error heuristics; Python could replace an invalid value with a prior one. | Linux and Prusa treat the serial transaction as one acquisition operation; Elegoo keeps this low-level logic inside its MCU blob. | **MCU owns validity** and emits a quality word with every raw record. Invalid values are diagnostic-only. | +| Startup | Data was accepted immediately after command/restart. | The data sheet requires four settling conversions; upstream also has reset/start sequencing. | Explicit power-down/reset, 50-ms start delay, five discarded and two qualified frames. | +| DOUT protocol | A missed/late line could become a normal sample or a host-side `100000`-count error. | Stock firmware waits for ready, shifts 24 + gain bits under a protected timing window; no source supports fabricating data. | Pre-ready, gain-bit, post-read-low, overrun, and rail checks become quality flags; protocol faults reset the acquisition state. | +| Triggering | One filtered sample could trigger; `abs(INT32_MIN)` was unsafe. | Noise must be rejected before a physical endstop decision. | Invalid acquisition immediately reports a homing fault; two consecutive qualified filtered samples are required to trigger. | +| Tare | Arithmetic mean over a short time window. | Elegoo calibrates over 30 samples; median resists a remaining impulsive sample. | Per-channel 30-sample median baseline. | +| Filter updates | The command-time override could update the wrong design state. | Filter state and coefficients must change atomically at the defined boundary. | Correct command object/state update and signed fixed-point bounds. | + +The decompiled Elegoo 1.6.9 and 1.7.0 images show the same essential +low-level pattern: bounded ready polling, 24-bit shift plus gain clocks, +sign extension, and protection around the transfer. They do **not** expose +the higher-level policy, so they support the protocol decisions but not a +claim that their opaque filtering/threshold policy is superior. + +## Chosen implementation + +Kalico commit `8433f46d569694ab136fc90696031ec68ef4270d` implements the +following contract. + +1. `sensor_hx711s.c` appends `sensor_count` signed raw counts followed by one + unsigned 32-bit quality word. Bits 8–11 identify affected channels; + low bits identify settling, not-ready, bad gain clocks, post-read-low, + capture overrun, and exact ADC rails. +2. `hx711s.py` drops any nonzero-quality frame before it reaches a client. It + retains `{time, counts, quality}` in the batch's `faults` field for logs + and diagnostics. There is no held-value synthesis, fixed count threshold, + or Python-side acquisition restart. +3. The MCU performs recovery. A protocol fault starts a deterministic reset; + settling and qualifying records remain visible but are never control data. +4. If a probe is active, an invalid frame ends the move with the dedicated + `invalid HX711 sample` trsync reason. This is deliberately fail-safe: + continuing Z motion after a broken acquisition is worse than a retry. +5. Valid filtered force must cross the configured threshold twice in a row. + At the normal 80 SPS this costs at most 12.5 ms and removes the last + plausible single-frame trigger path. +6. All other bulk ADC implementations explicitly emit an empty `faults` list + so the common `BulkAdcData` interface stays uniform. + +### Rejected alternatives + +* **Keep a last known-good value.** It makes graphs look continuous, but it + falsely asserts a force that was never measured and can contaminate tare, + filtering, and a probe decision. Rejected. +* **Retry only in Python.** It cannot establish whether SCK timing or the + gain-selection pulses were valid. It also races MCU-triggered homing. + Rejected. +* **Ignore a single bad channel in a four-cell sum.** This turns a hardware + disagreement into a plausible but wrong force and hides a wiring/thermal + fault. Rejected for probing; a future diagnostic-only degraded mode may be + useful, but must never drive Z motion. +* **Longer digital filtering instead of frame validity.** It may hide bad + data but cannot distinguish a real load from a malformed serial transfer. + Rejected as the primary defense. +* **One very long startup discard.** Simple but needlessly delays a probe. + The selected 5+2 sequence honors the data-sheet four-conversion rule and + separately proves two usable frames. + +## Incident interpretation + +The current issue evidence includes repeated `192`-count glitches, commonly +on channels 1/2, and occasional `-1` values during hot-chamber probing +(>50 C chamber, 105 C bed). Those are acquisition faults this design makes +visible and fail-safe rather than silently substituting. + +One later `sensor_bulk_status` error in the same log must **not** be counted +as an independent HX711 fault: it followed `Heater heater_bed not heating at +expected rate` and the resulting global emergency shutdown. The probe +failures and 192/-1 glitches remain the relevant HX711 evidence. + +## Validation and rollout checklist + +The code has passed Python byte-compilation and an STM32F401 bed-MCU build +using the CC1 configuration. It has not yet earned a reliability claim on a +printer. Before enabling it for normal operation: + +1. Build the Cosmos image with the recipe revision below; confirm its Kalico + host package and bed-MCU firmware both derive from `8433f46d`. +2. Flash an idle CC1, restart Klipper, and confirm four clean channels in the + load-cell status stream after the startup discard period. +3. Run at least 30 unloaded tare samples; record per-channel medians and + spread. A large channel spread is a hardware diagnostic, not a reason to + relax validity checks. +4. Run repeated cold bed probes, then controlled hot tests at 105 C bed and + >50 C chamber. Record every quality word, trsync error, false trigger, + timeout, and successful probe position. +5. Inject/observe a disconnected DOUT or forced not-ready condition only + while the machine is safe. Verify that active homing aborts without Z + overtravel and that normal sampling recovers after reset. +6. Compare repeated-probe repeatability and failure rate against the current + firmware. Do not tune the trigger threshold merely to mask a channel + quality fault. +7. If a valid, stable sample stream still exhibits false triggers, add a + configurable per-channel disagreement limit and report it separately from + protocol quality. Keep that policy outside the serial framing code. + +## Cosmos integration + +`meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc` should point +at branch `paul/hx711-reliability`, revision +`8433f46d569694ab136fc90696031ec68ef4270d`, remove the obsolete +`0003-fix-hx711s-mcu-glitch-filter.patch`, and increment `PR` to `r9`. +That patch was a narrow stopgap for duplicate callbacks; the new revision +removes the divergence instead of layering another conflicting fix on top. diff --git a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg index 23dcda23..eefb3ff2 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg @@ -524,7 +524,7 @@ gcode: {% for _ in range(2) %} G90 G1 X120 Y-1 F9000 - PROBE HOME=Z SAMPLES=3 SAMPLES_TOLERANCE=0.05 BAD_PROBE_RETRIES=3 BAD_PROBE_STRATEGY=RETRY ; Cleaning passes + PROBE HOME=Z SAMPLES=3 SAMPLES_TOLERANCE=0.1 BAD_PROBE_RETRIES=3 BAD_PROBE_STRATEGY=RETRY ; Cleaning passes G91 G0 X4 F600 G0 Z0.8 F300 ; Using pressure of bending the build plate to squish filament diff --git a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc index 6b8b6c71..c8754d86 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -4,7 +4,7 @@ LIC_FILES_CHKSUM = "file://COPYING;md5=1ebbd3e34237af26da5dc08a4e440464" FILESEXTRAPATHS:prepend := "${THISDIR}/files:" -SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=hx711s-new \ +SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/hx711-reliability \ file://0001-remove-save-config-subfile-check.patch \ file://0002-reduce-calibration-difference-tolerance.patch \ file://0001-Reduce-log-rotate-threshold.patch \ @@ -13,9 +13,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=hx711s file://0001-probe-home-z-includes-z-offset.patch \ file://0001-force-specific-input-shaper.patch \ " -SRCREV = "413e9e3886463d42b5c3fad678969962dac1ab0f" +SRCREV = "c368f84ec516f5d639aeddcb99d2c967957ab8e7" -PR = "r8" +PR = "r10" S = "${WORKDIR}/git"