Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions HX711.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion meta-opencentauri/recipes-apps/klipper/files/macros.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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"

Expand Down
Loading