From e1fb70d2788efe8ede9305a1935e59b30823099b Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:30:38 -0400 Subject: [PATCH 1/2] loadcell: make HX711 frame validity MCU-owned Pin Kalico to the reliability implementation that emits explicit frame quality, resets and qualifies acquisition deterministically, rejects invalid data before filtering or tare, and fails an active probe safely. Remove the superseded host/MCU glitch-filter patch. Document the source comparison, design decisions, hot-chamber incident evidence, and required on-printer validation. --- HX711.md | 156 ++++++++++++++++++ .../klipper/kalico_2026.02.00.inc | 6 +- 2 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 HX711.md 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/kalico_2026.02.00.inc b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc index 6b8b6c71..cf3aad3c 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 = "8433f46d569694ab136fc90696031ec68ef4270d" -PR = "r8" +PR = "r9" S = "${WORKDIR}/git" From c0856bd1f6b2511f7f0d6b9358e9860d6351537b Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:26:21 -0400 Subject: [PATCH 2/2] kalico: bump to c368f84e (validated HX711 mesh stack) + relax cleaning-pass tolerance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kalico paul/hx711-reliability @ c368f84e is the five-iteration stack that completed 10/10 consecutive BED_MESH_CALIBRATE BED_TEMP=60 runs on the CC1 test printer (zero hard faults in 392k frames): - timing-miss reclassification (skips instead of resets) - drift-safety band gates arming sample only - first-crossing trigger default (upstream semantics) - per-channel spike rejection (>100k single-sample glitches) - bounded bulk-overflow tolerance (<=2/session) macros.cfg: LOADCELL_Z_HOME cleaning passes get SAMPLES_TOLERANCE=0.1 — they are ooze management, not measurement, and the squish cycles mangle the edge contact; the final center probe keeps 0.05. --- meta-opencentauri/recipes-apps/klipper/files/macros.cfg | 2 +- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 cf3aad3c..c8754d86 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -13,9 +13,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0001-probe-home-z-includes-z-offset.patch \ file://0001-force-specific-input-shaper.patch \ " -SRCREV = "8433f46d569694ab136fc90696031ec68ef4270d" +SRCREV = "c368f84ec516f5d639aeddcb99d2c967957ab8e7" -PR = "r9" +PR = "r10" S = "${WORKDIR}/git"