diff --git a/klippy/extras/load_cell/hx711s.py b/klippy/extras/load_cell/hx711s.py index 2f5f494cc9..f30dfab802 100644 --- a/klippy/extras/load_cell/hx711s.py +++ b/klippy/extras/load_cell/hx711s.py @@ -16,6 +16,7 @@ SAMPLE_ERROR_DESYNC = -0x80000000 SAMPLE_ERROR_READ_TOO_LONG = 0x40000000 SAMPLE_ERROR_TORN_READ = 0x20000000 +SAMPLE_ERROR_RECOVERED = 0x10000000 ADC_FACTOR = 1.0 / (1 << 23) # A corrupt chip frame can pass framing yet sit hundreds of thousands of # counts from the truth. A single chip's reading moves by well under this @@ -40,6 +41,8 @@ def __init__( self.consecutive_fails = 0 self.torn_read_count = 0 self.spike_count = 0 + self.torn_retry_total = 0 + self.recovered_count = 0 # last valid per-channel counts, held across torn reads and glitches # to keep the sample stream gap-free (see _convert_samples) self._last_channel_counts = None @@ -189,6 +192,20 @@ def _convert_samples(self, samples): self.last_error_count += 1 logging.error("%s: READ_TOO_LONG at t=%.3f", self.name, ptime) break # errors latch in the MCU, the rest are duplicates + elif val == SAMPLE_ERROR_RECOVERED: + # The MCU power-cycled the chips in-driver (datasheet + # recovery) and discarded the settling conversions; the + # stream paused ~115ms instead of faulting. Marker frame: + # ch1 = torn-read retry tally, ch2 = recovery tally. + self.torn_retry_total += channel_counts[1] + self.recovered_count = channel_counts[2] + logging.warning( + "%s: in-driver recovery at t=%.3f (recoveries=%d," + " torn retries=%d)", + self.name, ptime, self.recovered_count, + self.torn_retry_total, + ) + continue # marker, not a force sample elif self._is_glitch(channel_counts, ptime): # corrupt frame that passed framing; hold the last good # reading so the curve stays clean for tap analysis diff --git a/src/sensor_hx711s.c b/src/sensor_hx711s.c index 8ea24d223e..819702e4b1 100644 --- a/src/sensor_hx711s.c +++ b/src/sensor_hx711s.c @@ -20,23 +20,63 @@ #define SAMPLE_ERROR_DESYNC (1L << 31) #define SAMPLE_ERROR_READ_TOO_LONG (1L << 30) #define SAMPLE_ERROR_TORN_READ (1L << 29) +#define SAMPLE_ERROR_RECOVERED (1L << 28) #define HX711S_OVERFLOW (1 << 1) +// Read-on-DRDY recovery design (see HX711F datasheet and the reference +// drivers cited in hx711s_read_adc): a frame that raced a conversion is +// re-read on the next DRDY instead of being consumed or discarded; a +// protocol desync is cleared by an in-driver power cycle with the +// datasheet-mandated settling conversions discarded. Neither condition +// reaches the host as a fatal error. +#define HX711S_MAX_TORN_RETRIES 2 +#define HX711S_SETTLING_FRAMES 4 // datasheet: settling conversions at 80SPS +#define HX711S_STUCK_US 31000 // ~2.5 conversion periods at 80 SPS + +// recovery_state values +#define RECOVERY_NONE 0 +#define RECOVERY_PD_HIGH 1 // SCK held high: power-down pulse in progress +#define RECOVERY_WAKE_WAIT 2 // chips woken, waiting out the 50ms settle +#define RECOVERY_SETTLING 3 // discarding the settling conversions + // A corrupt chip frame can pass the framing checks yet carry a value // hundreds of thousands of counts from the truth. Real contact force moves // each channel by well under this between samples (a 2mm/s approach at 80 // SPS), so a larger single-sample jump is a glitch, not a load change. #define SPIKE_CHANNEL_THRESHOLD 100000 +// Move accel kicks and travel vibration produce single-sample force +// impulses that fully revert on the next sample; captured on the CC1 test +// printer from raw force streams and tap curves (2026-07-26): ~220g blips +// at move starts and an isolated +87g blip 0.5s after travel settled, one +// sample wide. Any such impulse that crosses trigger_force fires a phantom +// first-crossing trigger ("probe triggered prior to movement" or a flat +// tap window -> TAP_CHRONOLOGY). Real contact at 2mm/s rises by at most +// ~80g/sample and is SUSTAINED. So any single-sample jump larger than the +// trigger force is held for one sample and only forwarded if the next +// sample confirms it (sign-aware: an up-jump confirms if the next sample +// stays above pending - threshold, which steep real ramps satisfy). +#define SPIKE_SUM_THRESHOLD 7000 // ~67g, just under trigger_force (75g) + struct hx711s_adc { struct timer timer; uint32_t rest_ticks; uint32_t last_error; + uint32_t not_ready_since; int32_t last_good_counts[MAX_SENSORS]; int32_t pending_counts[MAX_SENSORS]; + int32_t last_good_sum; + int32_t pending_sum; + uint16_t torn_retry_total; + uint16_t recovered_total; uint8_t have_last_counts; uint8_t have_pending_counts; + uint8_t have_last_sum; + uint8_t have_pending_sum; uint8_t pending_flag; + uint8_t recovery_state; + uint8_t settle_remaining; + uint8_t torn_retry_count; uint8_t sensor_count; uint8_t gain_channel; // extra clock pulses: chip type + gain selection uint8_t sample_bytes; // sensor_count * BYTES_PER_SAMPLE @@ -48,6 +88,8 @@ struct hx711s_adc { static struct task_wake wake_hx711s; +static void hx711s_start_recovery(struct hx711s_adc *h); + /**************************************************************** * Low-level bit-banging @@ -128,19 +170,73 @@ hx711s_event(struct timer *timer) { struct hx711s_adc *h = container_of(timer, struct hx711s_adc, timer); uint32_t rest_ticks = h->rest_ticks; + + // Recovery state machine. Power-down per HX711F datasheet ("when PD_SCK + // is high for more than 60us the chip enters power down mode"); after + // wake the output needs the datasheet settling time before data is + // trusted, so frames are discarded in the read task. + if (h->recovery_state == RECOVERY_PD_HIGH) { + for (uint8_t i = 0; i < h->sensor_count; i++) + gpio_out_write(h->clks[i], 0); // release power down + h->recovery_state = RECOVERY_WAKE_WAIT; + h->timer.waketime += timer_from_us(50000); + return SF_RESCHEDULE; + } + if (h->recovery_state == RECOVERY_WAKE_WAIT) { + h->recovery_state = RECOVERY_SETTLING; + h->timer.waketime += rest_ticks; + return SF_RESCHEDULE; + } + if (h->recovery_state == RECOVERY_SETTLING) { + if (!h->pending_flag && hx711s_is_data_ready(h)) { + h->pending_flag = 1; + sched_wake_task(&wake_hx711s); + } + h->timer.waketime += rest_ticks; + return SF_RESCHEDULE; + } + if (h->pending_flag) { h->sb.possible_overflows++; h->pending_flag |= HX711S_OVERFLOW; rest_ticks *= 4; } else if (hx711s_is_data_ready(h)) { + h->not_ready_since = 0; h->pending_flag = 1; sched_wake_task(&wake_hx711s); rest_ticks *= 8; + } else { + // No DRDY for ~2.5 conversion periods means a chip wedged (its + // oscillator free-runs, so silence is never legitimate). Recover + // in-driver rather than escalating a dead stream to the host. + uint32_t now = timer_read_time(); + if (!h->not_ready_since) + h->not_ready_since = now; + else if (now - h->not_ready_since > timer_from_us(HX711S_STUCK_US)) + hx711s_start_recovery(h); } h->timer.waketime += rest_ticks; return SF_RESCHEDULE; } +// Power-cycle all chips and re-arm acquisition. A desynced gain-selection +// state is only cleared this way (HX711F datasheet, power-down control); +// Elegoo's stock strain-gauge firmware performs the same recovery. +static void +hx711s_start_recovery(struct hx711s_adc *h) +{ + for (uint8_t i = 0; i < h->sensor_count; i++) + gpio_out_write(h->clks[i], 1); // SCK high >60us: power down + h->recovery_state = RECOVERY_PD_HIGH; + h->settle_remaining = HX711S_SETTLING_FRAMES; + h->pending_flag = 0; + h->have_last_counts = 0; + h->have_pending_counts = 0; + h->not_ready_since = 0; + h->torn_retry_count = 0; + h->timer.waketime = timer_read_time() + timer_from_us(1000); +} + static void append_sample(struct hx711s_adc *h, int32_t val) { @@ -151,6 +247,56 @@ append_sample(struct hx711s_adc *h, int32_t val) h->sb.data_count += BYTES_PER_SAMPLE; } +// Forward a summed sample to the probe trigger unless it is a single-sample +// impulse (see SPIKE_SUM_THRESHOLD): the first jump is held for one sample +// and only released if the next sample confirms it. The raw per-channel +// stream above is unaffected, so the host still sees the impulse for +// diagnostics. Same hold-confirm pattern as the per-channel glitch filter. +static void +report_sum(struct hx711s_adc *h, int32_t sum) +{ + if (!h->have_last_sum) { + h->last_good_sum = sum; + h->have_last_sum = 1; + if (h->lce) + load_cell_probe_report_sample(h->lce, sum); + return; + } + int32_t delta = sum - h->last_good_sum; + if (delta > SPIKE_SUM_THRESHOLD || delta < -SPIKE_SUM_THRESHOLD) { + if (h->have_pending_sum) { + // sign-aware confirm: the step is genuine if the level held; + // a steep real ramp keeps rising past the pending value, a + // phantom impulse reverts below it + int32_t confirm = (delta > 0) + ? (sum >= h->pending_sum - SPIKE_SUM_THRESHOLD) + : (sum <= h->pending_sum + SPIKE_SUM_THRESHOLD); + if (confirm) { + // two consecutive samples agree on the new value: genuine + // sustained step, release the held sample and this one + if (h->lce) + load_cell_probe_report_sample(h->lce, h->pending_sum); + h->last_good_sum = h->pending_sum; + h->have_pending_sum = 0; + } else { + // still jumping: track the latest suspect value + h->pending_sum = sum; + return; + } + } else { + h->pending_sum = sum; + h->have_pending_sum = 1; + return; + } + } else if (h->have_pending_sum) { + // the held sample reverted: it was an impulse, drop it + h->have_pending_sum = 0; + } + h->last_good_sum = sum; + if (h->lce) + load_cell_probe_report_sample(h->lce, sum); +} + static void hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) { @@ -160,6 +306,29 @@ hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) uint32_t adc[MAX_SENSORS]; uint32_t sample_error = 0; + // During recovery the datasheet requires discarding the settling + // conversions after a power cycle (4 at 80 SPS); nothing is emitted + // until they are done, then the host gets one RECOVERED marker. + if (h->recovery_state == RECOVERY_SETTLING) { + uint32_t junk[MAX_SENSORS]; + hx711s_raw_read(h, junk, 24 + gain_channel); + irq_disable(); + h->pending_flag = 0; + irq_enable(); + if (--h->settle_remaining == 0) { + h->recovery_state = RECOVERY_NONE; + h->recovered_total++; + append_sample(h, SAMPLE_ERROR_RECOVERED); + append_sample(h, (int32_t)h->torn_retry_total); + append_sample(h, (int32_t)h->recovered_total); + append_sample(h, SAMPLE_ERROR_RECOVERED); + h->torn_retry_total = 0; + if (h->sb.data_count + h->sample_bytes > ARRAY_SIZE(h->sb.data)) + sensor_bulk_report(&h->sb, oid); + } + return; + } + // The chips free-run on independent oscillators, so a chip may latch a // new conversion into its output register between the data-ready poll // and this read, or during the read itself. Bits clocked out across a @@ -177,6 +346,34 @@ hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) torn_read = 1; } + // Capture and clear pending flag after all reads + irq_disable(); + uint8_t flags = h->pending_flag; + h->pending_flag = 0; + irq_enable(); + + if (torn_read) { + // Every reference driver waits for DRDY and re-reads rather than + // consume a frame that raced a conversion (Prusa HX717 + // src/common/hx717.cpp; Linux IIO drivers/iio/adc/hx711.c; upstream + // Klipper src/sensor_hx71x.c; Elegoo's bounded ready-poll). Do the + // same: emit nothing now and let the next DRDY drive a re-read. + // Bounded so a persistently mistimed chip degrades to the host's + // benign hold-last-value path instead of stalling the stream. + h->torn_retry_total++; + if (h->torn_retry_count < HX711S_MAX_TORN_RETRIES) { + h->torn_retry_count++; + return; + } + h->torn_retry_count = 0; + for (uint8_t i = 0; i < h->sensor_count; i++) + append_sample(h, (int32_t)SAMPLE_ERROR_TORN_READ); + if (h->sb.data_count + h->sample_bytes > ARRAY_SIZE(h->sb.data)) + sensor_bulk_report(&h->sb, oid); + return; + } + h->torn_retry_count = 0; + for (uint8_t i = 0; i < h->sensor_count; i++) { uint32_t raw = adc[i] >> gain_channel; if (raw & 0x800000) @@ -186,24 +383,20 @@ hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) sample_error = SAMPLE_ERROR_DESYNC; } - // Capture and clear pending flag after all reads - irq_disable(); - uint8_t flags = h->pending_flag; - h->pending_flag = 0; - irq_enable(); - if (flags & HX711S_OVERFLOW) sample_error = SAMPLE_ERROR_READ_TOO_LONG; - // Torn reads are a normal consequence of chip oscillator drift: discard - // just that sample, the next conversion is valid. They can also corrupt - // the extra-bit check, so they must take priority over a desync. A real - // desync corrupts the serial protocol state, making all further values - // unreliable until the chips are power cycled: latch it and keep - // reporting it so the host restarts the sensor. - if (torn_read) - sample_error = SAMPLE_ERROR_TORN_READ; - else if (sample_error) + // A desync corrupts the serial protocol state (the gain-selection pulse + // count no longer matches the chip), which only a power cycle clears + // (HX711F datasheet). Recover in-driver: the stream pauses for the + // ~115ms power-down/settle/discard sequence instead of faulting the + // host, which previously force-restarted the sensor and killed any + // active probe session. + if (sample_error == SAMPLE_ERROR_DESYNC) { + hx711s_start_recovery(h); + return; + } + if (sample_error) h->last_error = sample_error; if (h->last_error) @@ -230,8 +423,7 @@ hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) for (uint8_t i = 0; i < h->sensor_count; i++) h->last_good_counts[i] = counts_buf[i]; h->have_last_counts = 1; - if (h->lce) - load_cell_probe_report_sample(h->lce, sum); + report_sum(h, sum); } else { for (uint8_t i = 0; i < h->sensor_count; i++) { int32_t delta = counts_buf[i] - h->last_good_counts[i]; @@ -246,8 +438,7 @@ hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) h->have_pending_counts = 0; for (uint8_t i = 0; i < h->sensor_count; i++) h->last_good_counts[i] = counts_buf[i]; - if (h->lce) - load_cell_probe_report_sample(h->lce, sum); + report_sum(h, sum); } else if (h->have_pending_counts) { uint8_t pending_match = 1; for (uint8_t i = 0; i < h->sensor_count; i++) { @@ -265,8 +456,7 @@ hx711s_read_adc(struct hx711s_adc *h, uint8_t oid) h->have_pending_counts = 0; for (uint8_t i = 0; i < h->sensor_count; i++) h->last_good_counts[i] = counts_buf[i]; - if (h->lce) - load_cell_probe_report_sample(h->lce, sum); + report_sum(h, sum); } else { // Replace a stale pending glitch with the latest sample. for (uint8_t i = 0; i < h->sensor_count; i++) @@ -339,6 +529,14 @@ command_query_hx711s(uint32_t *args) h->last_error = 0; h->have_last_counts = 0; h->have_pending_counts = 0; + h->have_last_sum = 0; + h->have_pending_sum = 0; + h->recovery_state = RECOVERY_NONE; + h->settle_remaining = 0; + h->torn_retry_count = 0; + h->torn_retry_total = 0; + h->recovered_total = 0; + h->not_ready_since = 0; h->rest_ticks = args[1]; if (!h->rest_ticks) { for (uint8_t i = 0; i < h->sensor_count; i++)