From 3a700a5f62f7b8b0be698de2a16490d0a8cf6c2b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 24 Nov 2025 17:09:41 -0500 Subject: [PATCH 001/108] timer_irq: Remove TIMER_IDLE_REPEAT_TICKS special case The TIMER_IDLE_REPEAT_TICKS was intended to reduce the number of cases where timers would defer to tasks when tasks are mostly idle. However, with commit ea546c78 this became less important because timers now only defer to tasks if tasks are consistently busy for two consecutive calls to sched_check_set_tasks_busy(). The TIMER_IDLE_REPEAT_TICKS mechanism could result in extended task delays if timers do become busy. Timers can become busy in normal operation if timers are scheduled to run more than 500,000 times a second (every 2us or faster). This can occur on stepper movement when using high microstep settings. If timers become busy, it could take up to 1ms for tasks to next be given an opportunity to run (two calls to sched_check_set_tasks_busy() at 500us per call). This wouldn't typically be an issue if tasks are also busy, but in some loads tasks may need to run periodically in such a way that the task status alternates between idle and busy. In this case, the TIMER_IDLE_REPEAT_TICKS mechanism could effectively limit the number of task wakeups to only ~1000 per second. The "USB to canbus bridge" code uses tasks to transfer data to/from USB and canbus. If timers become busy, the limiting of task wakeups could lead to a situation where the effective bandwidth becomes severely constrained. In particular, this can be an issue on USB implementations that do not support "double buffering" (such as the stm32 usbotg code). There are reports of "Timer too close" errors on "USB to canbus bridge" mode as a result of this issue. Fix by removing the TIMER_IDLE_REPEAT_TICKS check. Check for busy tasks every TIMER_REPEAT_TICKS instead (100us). Signed-off-by: Kevin O'Connor --- src/avr/timer.c | 3 +-- src/generic/armcm_timer.c | 3 +-- src/generic/timer_irq.c | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/avr/timer.c b/src/avr/timer.c index d306b7214..ef6269f06 100644 --- a/src/avr/timer.c +++ b/src/avr/timer.c @@ -154,7 +154,6 @@ static struct timer wrap_timer = { .waketime = 0x8000, }; -#define TIMER_IDLE_REPEAT_TICKS 8000 #define TIMER_REPEAT_TICKS 3000 #define TIMER_MIN_ENTRY_TICKS 44 @@ -202,7 +201,7 @@ ISR(TIMER1_COMPA_vect) next = now + TIMER_DEFER_REPEAT_TICKS; goto done; } - timer_repeat_set(now + TIMER_IDLE_REPEAT_TICKS); + timer_repeat_set(now + TIMER_REPEAT_TICKS); timer_set(now); } } diff --git a/src/generic/armcm_timer.c b/src/generic/armcm_timer.c index e77081804..b7f572685 100644 --- a/src/generic/armcm_timer.c +++ b/src/generic/armcm_timer.c @@ -112,7 +112,6 @@ timer_init(void) DECL_INIT(timer_init); static uint32_t timer_repeat_until; -#define TIMER_IDLE_REPEAT_TICKS timer_from_us(500) #define TIMER_REPEAT_TICKS timer_from_us(100) #define TIMER_MIN_TRY_TICKS timer_from_us(2) @@ -141,7 +140,7 @@ timer_dispatch_many(void) timer_repeat_until = now + TIMER_REPEAT_TICKS; return TIMER_DEFER_REPEAT_TICKS; } - timer_repeat_until = tru = now + TIMER_IDLE_REPEAT_TICKS; + timer_repeat_until = tru = now + TIMER_REPEAT_TICKS; } // Next timer in the past or near future - wait for it to be ready diff --git a/src/generic/timer_irq.c b/src/generic/timer_irq.c index 7c2e871bd..9f6b59aa3 100644 --- a/src/generic/timer_irq.c +++ b/src/generic/timer_irq.c @@ -30,7 +30,6 @@ timer_is_before(uint32_t time1, uint32_t time2) } static uint32_t timer_repeat_until; -#define TIMER_IDLE_REPEAT_TICKS timer_from_us(500) #define TIMER_REPEAT_TICKS timer_from_us(100) #define TIMER_MIN_TRY_TICKS timer_from_us(2) @@ -59,7 +58,7 @@ timer_dispatch_many(void) timer_repeat_until = now + TIMER_REPEAT_TICKS; return now + TIMER_DEFER_REPEAT_TICKS; } - timer_repeat_until = tru = now + TIMER_IDLE_REPEAT_TICKS; + timer_repeat_until = tru = now + TIMER_REPEAT_TICKS; } // Next timer in the past or near future - wait for it to be ready From a6a6b21e4d2f289737d16d938632df7b73ed492e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 24 Nov 2025 19:15:02 -0500 Subject: [PATCH 002/108] armcm_timer: Use a static instruction count for TIMER_MIN_TRY_TICKS Change TIMER_MIN_TRY_TICKS from 2us to 90 instructions. On newer chips 2us is a large amount of time - for example on the 520Mhz stm32h723 it would be 1040 instructions. Using a large time can result in "busy waiting" in the irq handler when the cpu may be better spent running tasks. The armcm_timer.c code is used on most ARM cortex-M chips and on all of these chips the SysTick timer should be tied directly to the instruction counter. This change should be safe because it should not take more than 90 instructions to reschedule the timer on any of these chips. Also, all of these chips should be able to exit the irq handler and reenter it in less than 90 instructions allowing more time for tasks to run if the next timer is more than 90 timer ticks in the future. Signed-off-by: Kevin O'Connor --- src/generic/armcm_timer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generic/armcm_timer.c b/src/generic/armcm_timer.c index b7f572685..e2d77924d 100644 --- a/src/generic/armcm_timer.c +++ b/src/generic/armcm_timer.c @@ -114,7 +114,7 @@ DECL_INIT(timer_init); static uint32_t timer_repeat_until; #define TIMER_REPEAT_TICKS timer_from_us(100) -#define TIMER_MIN_TRY_TICKS timer_from_us(2) +#define TIMER_MIN_TRY_TICKS 90 #define TIMER_DEFER_REPEAT_TICKS timer_from_us(5) // Invoke timers From 2e5802370c1ab944373814f96f9d35a3db9d3143 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 24 Nov 2025 19:30:35 -0500 Subject: [PATCH 003/108] serialqueue: Tune MIN_REQTIME_DELTA timing The MIN_REQTIME_DELTA parameter controls when the host will flush incomplete message blocks to the mcu. If the message had a target time less than 250ms it would result in a flush even if a message block was not completely full. In the situation where the host generates lots of queue_step commands to the point that it fills the mcu move_queue, then it would be possible for individual queue_step commands to become eligible for transmit only microseconds apart. It could also lead to a situation where the target time was less than 250ms in the future. The result could lead to many small message blocks as each became flushed individually. Tune the MIN_REQTIME_DELTA to 100ms to reduce the chance of this. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index d30433554..dab6b8a1a 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -107,7 +107,7 @@ struct serialqueue { #define MIN_RTO 0.025 #define MAX_RTO 5.000 #define MAX_PENDING_BLOCKS 12 -#define MIN_REQTIME_DELTA 0.250 +#define MIN_REQTIME_DELTA 0.100 #define MIN_BACKGROUND_DELTA 0.005 #define IDLE_QUERY_TIME 1.0 From 2e88d8b5dfdcf0e8ea826166cb0ad1677390aafc Mon Sep 17 00:00:00 2001 From: Benjie Date: Tue, 9 Dec 2025 18:15:15 +0000 Subject: [PATCH 004/108] config: Clarify configuration settings for Ender 5 Pro (#7126) Signed-off-by: Benjie Gillam --- config/printer-creality-ender5pro-2020.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/printer-creality-ender5pro-2020.cfg b/config/printer-creality-ender5pro-2020.cfg index ded26b408..2321cfb6d 100644 --- a/config/printer-creality-ender5pro-2020.cfg +++ b/config/printer-creality-ender5pro-2020.cfg @@ -1,7 +1,7 @@ # This file contains pin mappings for the stock 2020 Creality Ender 5 # Pro with the 32-bit Creality 4.2.2 board. To use this config, during # "make menuconfig" select the STM32F103 with a "28KiB bootloader" and -# with "Use USB for communication" disabled. +# communication interface set to "Serial (on USART1 PA10/PA9)". # If you prefer a direct serial connection, in "make menuconfig" # select "Enable extra low-level configuration options" and select the From f9108496a18f158221d20a53197acc04d9c9b5f9 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 27 Nov 2025 01:26:52 +0100 Subject: [PATCH 005/108] ldc1612: fix data rate calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no need to remove 4 from data rate. Formula for conversion time is: (RCOUNT0×16)/ƒREF0 Signed-off-by: Timofey Titovets --- klippy/extras/ldc1612.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index dd41b43ae..973556af1 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -176,7 +176,7 @@ class LDC1612: "(e.g. faulty wiring) or a faulty ldc1612 chip." % (manuf_id, dev_id, LDC1612_MANUF_ID, LDC1612_DEV_ID)) # Setup chip in requested query rate - rcount0 = self.frequency / (16. * (self.data_rate - 4)) + rcount0 = self.frequency / (16. * self.data_rate) self.set_reg(REG_RCOUNT0, int(rcount0 + 0.5)) self.set_reg(REG_OFFSET0, 0) self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.frequency/16. + .5)) From 2b4c55ffd118a4982cfb04a01052746bb8cb45d9 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 3 Dec 2025 01:12:53 +0100 Subject: [PATCH 006/108] servo: sync pwm clock times Arriving of SW PWM out of sync can cause pulse width distortion - make them longer Synchronize the update clock to avoid that Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/multi_pin.py | 2 ++ klippy/extras/output_pin.py | 9 +++++++++ klippy/extras/replicape.py | 2 ++ klippy/extras/servo.py | 7 ++++++- klippy/extras/sx1509.py | 2 ++ klippy/mcu.py | 17 +++++++++++++++++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/klippy/extras/multi_pin.py b/klippy/extras/multi_pin.py index c834ee077..f126f928c 100644 --- a/klippy/extras/multi_pin.py +++ b/klippy/extras/multi_pin.py @@ -46,6 +46,8 @@ class PrinterMultiPin: def set_digital(self, print_time, value): for mcu_pin in self.mcu_pins: mcu_pin.set_digital(print_time, value) + def next_aligned_print_time(self, print_time, allow_early=0.): + return print_time def set_pwm(self, print_time, value): for mcu_pin in self.mcu_pins: mcu_pin.set_pwm(print_time, value) diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index a51292990..a15d55166 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -46,6 +46,11 @@ class GCodeRequestQueue: if action == "discard": del rqueue[:pos+1] continue + if action == "reschedule": + del rqueue[:pos] + self.next_min_flush_time = max(self.next_min_flush_time, + min_wait) + continue if action == "delay": pos -= 1 del rqueue[:pos+1] @@ -75,6 +80,10 @@ class GCodeRequestQueue: action, min_wait = ret if action == "discard": break + if action == "reschedule": + self.next_min_flush_time = max(self.next_min_flush_time, + min_wait) + continue self.next_min_flush_time = next_time + max(min_wait, min_sched_time) if action != "delay": break diff --git a/klippy/extras/replicape.py b/klippy/extras/replicape.py index f7f7bb64b..eaca8b83d 100644 --- a/klippy/extras/replicape.py +++ b/klippy/extras/replicape.py @@ -67,6 +67,8 @@ class pca9685_pwm: cmd_queue = self._mcu.alloc_command_queue() self._set_cmd = self._mcu.lookup_command( "queue_pca9685_out oid=%c clock=%u value=%hu", cq=cmd_queue) + def next_aligned_print_time(self, print_time, allow_early=0.): + return print_time def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) if self._invert: diff --git a/klippy/extras/servo.py b/klippy/extras/servo.py index f1ce99763..303e39377 100644 --- a/klippy/extras/servo.py +++ b/klippy/extras/servo.py @@ -6,6 +6,7 @@ from . import output_pin SERVO_SIGNAL_PERIOD = 0.020 +RESCHEDULE_SLACK = 0.000500 class PrinterServo: def __init__(self, config): @@ -47,8 +48,12 @@ class PrinterServo: def _set_pwm(self, print_time, value): if value == self.last_value: return "discard", 0. + aligned_ptime = self.mcu_servo.next_aligned_print_time(print_time, + RESCHEDULE_SLACK) + if aligned_ptime > print_time + RESCHEDULE_SLACK: + return "reschedule", aligned_ptime self.last_value = value - self.mcu_servo.set_pwm(print_time, value) + self.mcu_servo.set_pwm(aligned_ptime, value) def _get_pwm_from_angle(self, angle): angle = max(0., min(self.max_angle, angle)) width = self.min_width + angle * self.angle_to_width diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 99df55df3..ce25bd027 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -178,6 +178,8 @@ class SX1509_pwm(object): self._shutdown_value = max(0., min(1., shutdown_value)) self._sx1509.set_register(self._i_on_reg, ~int(255 * self._start_value) & 0xFF) + def next_aligned_print_time(self, print_time, allow_early=0.): + return print_time def set_pwm(self, print_time, value): self._sx1509.set_register(self._i_on_reg, ~int(255 * value) if not self._invert diff --git a/klippy/mcu.py b/klippy/mcu.py index 14be8a5c0..d9bd34fb8 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -422,6 +422,7 @@ class MCU_pwm: self._invert = pin_params['invert'] self._start_value = self._shutdown_value = float(self._invert) self._last_clock = 0 + self._last_value = .0 self._pwm_max = 0. self._set_cmd = None def get_mcu(self): @@ -437,6 +438,7 @@ class MCU_pwm: shutdown_value = 1. - shutdown_value self._start_value = max(0., min(1., start_value)) self._shutdown_value = max(0., min(1., shutdown_value)) + self._last_value = self._start_value def _build_config(self): if self._max_duration and self._start_value != self._shutdown_value: raise pins.error("Pin with max duration must have start" @@ -488,6 +490,20 @@ class MCU_pwm: % (self._oid, self._last_clock, svalue), is_init=True) self._set_cmd = self._mcu.lookup_command( "queue_digital_out oid=%c clock=%u on_ticks=%u", cq=cmd_queue) + def next_aligned_print_time(self, print_time, allow_early=0.): + # Filter cases where there is no need to sync anything + if self._hardware_pwm: + return print_time + if self._last_value == 1. or self._last_value == .0: + return print_time + # Simplify the calling and allow scheduling slightly earlier + req_ptime = print_time - min(allow_early, 0.5 * self._cycle_time) + cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) + req_clock = self._mcu.print_time_to_clock(req_ptime) + last_clock = self._last_clock + pulses = (req_clock - last_clock + cycle_ticks - 1) // cycle_ticks + next_clock = last_clock + pulses * cycle_ticks + return self._mcu.clock_to_print_time(next_clock) def set_pwm(self, print_time, value): if self._invert: value = 1. - value @@ -496,6 +512,7 @@ class MCU_pwm: self._set_cmd.send([self._oid, clock, v], minclock=self._last_clock, reqclock=clock) self._last_clock = clock + self._last_value = value class MCU_adc: def __init__(self, mcu, pin_params): From f52a6f9491e6bbb6c04f3634012ae23f41e8b857 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 3 Dec 2025 17:35:12 -0500 Subject: [PATCH 007/108] output_pin: Rename "delay" flag to "repeat" in GCodeRequestQueue() Rename the flag to make it more clear what it does. Signed-off-by: Kevin O'Connor --- klippy/extras/fan.py | 2 +- klippy/extras/output_pin.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/klippy/extras/fan.py b/klippy/extras/fan.py index c5677ba06..2bcc512d7 100644 --- a/klippy/extras/fan.py +++ b/klippy/extras/fan.py @@ -63,7 +63,7 @@ class Fan: self.last_req_value = value self.last_fan_value = self.max_power self.mcu_fan.set_pwm(print_time, self.max_power) - return "delay", self.kick_start_time + return "repeat", print_time + self.kick_start_time self.last_fan_value = self.last_req_value = value self.mcu_fan.set_pwm(print_time, value) def set_speed(self, value, print_time=None): diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index a15d55166..58a7302ca 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -38,23 +38,23 @@ class GCodeRequestQueue: pos += 1 req_pt, req_val = rqueue[pos] # Invoke callback for the request - min_wait = 0. ret = self.callback(next_time, req_val) if ret is not None: # Handle special cases - action, min_wait = ret + action, next_min_time = ret + self.next_min_flush_time = max(self.next_min_flush_time, + next_min_time) if action == "discard": del rqueue[:pos+1] continue if action == "reschedule": del rqueue[:pos] - self.next_min_flush_time = max(self.next_min_flush_time, - min_wait) continue - if action == "delay": + if action == "repeat": pos -= 1 del rqueue[:pos+1] - self.next_min_flush_time = next_time + max(min_wait, min_sched_time) + self.next_min_flush_time = max(self.next_min_flush_time, + next_time + min_sched_time) # Ensure following queue items are flushed self.motion_queuing.note_mcu_movequeue_activity( self.next_min_flush_time, is_step_gen=False) @@ -73,19 +73,20 @@ class GCodeRequestQueue: while 1: next_time = max(print_time, self.next_min_flush_time) # Invoke callback for the request - action, min_wait = "normal", 0. + action, next_min_time = "normal", 0. ret = self.callback(next_time, value) if ret is not None: # Handle special cases - action, min_wait = ret + action, next_min_time = ret + self.next_min_flush_time = max(self.next_min_flush_time, + next_min_time) if action == "discard": break if action == "reschedule": - self.next_min_flush_time = max(self.next_min_flush_time, - min_wait) continue - self.next_min_flush_time = next_time + max(min_wait, min_sched_time) - if action != "delay": + self.next_min_flush_time = max(self.next_min_flush_time, + next_time + min_sched_time) + if action != "repeat": break From 8e6e467ebc16f93ab01ed63c55d24af52b020b54 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 15:31:09 -0500 Subject: [PATCH 008/108] mcu: Fix incorrect reqclock during endstop homing For correct operation the trsync system must be programmed prior to the start of endstop checking. This means the desired "reqclock" for the trsync configuration messages need to use the same "clock" that the endstop start message uses - even though the actual deadline for these messages is later. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index d9bd34fb8..507f77347 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -219,12 +219,11 @@ class MCU_trsync: self._mcu.register_response(self._handle_trsync_state, "trsync_state", self._oid) self._trsync_start_cmd.send([self._oid, report_clock, report_ticks, - self.REASON_COMMS_TIMEOUT], - reqclock=report_clock) + self.REASON_COMMS_TIMEOUT], reqclock=clock) for s in self._steppers: self._stepper_stop_cmd.send([s.get_oid(), self._oid]) self._trsync_set_timeout_cmd.send([self._oid, expire_clock], - reqclock=expire_clock) + reqclock=clock) def set_home_end_time(self, home_end_time): self._home_end_clock = self._mcu.print_time_to_clock(home_end_time) def stop(self): From 867d73f0b8988a8df79abb3223018132c11448ab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 15:55:57 -0500 Subject: [PATCH 009/108] serialqueue: Make 31-bit clock overflow check a little more robust Allow reqclock to be slightly less than the transmitted messages's deadline. That is, delay messages with a reqclock far in the future to slightly past (1<<31) ticks from its deadline. Use (3<<29) instead, which gives an additional (1<<29) grace period to avoid clock overflows. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index dab6b8a1a..3ab7216b9 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -886,9 +886,10 @@ serialqueue_send_batch(struct serialqueue *sq, struct command_queue *cq int len = 0; struct queue_message *qm; list_for_each_entry(qm, msgs, node) { - if (qm->min_clock + (1LL<<31) < qm->req_clock + if (qm->min_clock + (3LL<<29) < qm->req_clock && qm->req_clock != BACKGROUND_PRIORITY_CLOCK) - qm->min_clock = qm->req_clock - (1LL<<31); + // Avoid mcu clock comparison 31-bit overflow issues + qm->min_clock = qm->req_clock - (3LL<<29); len += qm->len; } if (! len) From d92dda439eb0f86a5a7235fa78878103be3e1de8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 12 Dec 2025 18:30:23 -0500 Subject: [PATCH 010/108] lib: Update pico-sdk to v2.2.0 The new rp2350 chips with A4 stepping require pico-sdk v2.2.0 . Signed-off-by: Kevin O'Connor --- lib/README | 2 +- lib/pico-sdk/boot/bootrom_constants.h | 344 ++++++++++++++++++ lib/pico-sdk/boot/picoboot_constants.h | 8 +- lib/pico-sdk/boot/uf2.h | 21 +- lib/pico-sdk/hardware/platform_defs.h | 3 + lib/pico-sdk/pico-sdk.patch | 26 ++ lib/pico-sdk/pico/bootrom_constants.h | 338 +---------------- lib/pico-sdk/pico/platform.h | 14 +- lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel | 5 + .../rp2040/boot_stage2/CMakeLists.txt | 22 +- .../rp2040/boot_stage2/boot2_w25x10cl.S | 4 +- .../rp2040/boot_stage2/boot_stage2.ld | 1 + .../boot_stage2/include/boot_stage2/config.h | 14 +- lib/pico-sdk/rp2040/hardware/platform_defs.h | 20 + lib/pico-sdk/rp2040/hardware/regs/intctrl.h | 18 + .../rp2040/hardware/structs/busctrl.h | 1 - lib/pico-sdk/rp2350/cmsis_include/RP2350.h | 10 +- .../rp2350/cmsis_include/system_RP2350.h | 14 +- lib/pico-sdk/rp2350/hardware/regs/clocks.h | 6 +- lib/pico-sdk/rp2350/hardware/regs/dreq.h | 8 +- .../rp2350/hardware/regs/glitch_detector.h | 6 +- lib/pico-sdk/rp2350/hardware/regs/intctrl.h | 36 +- lib/pico-sdk/rp2350/hardware/regs/pio.h | 6 +- lib/pico-sdk/rp2350/hardware/regs/powman.h | 59 +-- lib/pico-sdk/rp2350/hardware/regs/rosc.h | 14 +- lib/pico-sdk/rp2350/hardware/regs/rvcsr.h | 86 ++++- lib/pico-sdk/rp2350/hardware/regs/syscfg.h | 5 +- lib/pico-sdk/rp2350/hardware/regs/ticks.h | 36 +- lib/pico-sdk/rp2350/hardware/regs/usb.h | 7 +- .../rp2350/hardware/structs/busctrl.h | 1 - lib/pico-sdk/rp2350/hardware/structs/powman.h | 16 +- lib/pico-sdk/rp2350/hardware/structs/rosc.h | 4 +- lib/pico-sdk/rp2350/hardware/structs/syscfg.h | 2 +- src/rp2040/rp2350_bootrom.c | 4 +- 34 files changed, 662 insertions(+), 499 deletions(-) create mode 100644 lib/pico-sdk/boot/bootrom_constants.h diff --git a/lib/README b/lib/README index a106abfd0..387714105 100644 --- a/lib/README +++ b/lib/README @@ -107,7 +107,7 @@ taken from the Drivers/CMSIS/Device/ST/STM32H7xx/ directory. The pico-sdk directory contains code from the pico sdk: https://github.com/raspberrypi/pico-sdk.git -version 2.0.0 (efe2103f9b28458a1615ff096054479743ade236). It has been +version 2.2.0 (a1438dff1d38bd9c65dbd693f0e5db4b9ae91779). It has been modified so that it can build outside of the pico sdk. See pico-sdk.patch for the modifications. diff --git a/lib/pico-sdk/boot/bootrom_constants.h b/lib/pico-sdk/boot/bootrom_constants.h new file mode 100644 index 000000000..e7fe6f63b --- /dev/null +++ b/lib/pico-sdk/boot/bootrom_constants.h @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef _BOOT_BOOTROM_CONSTANTS_H +#define _BOOT_BOOTROM_CONSTANTS_H + +#ifndef NO_PICO_PLATFORM +#include "pico/platform.h" +#endif + +// ROOT ADDRESSES +#define BOOTROM_MAGIC_OFFSET 0x10 +#define BOOTROM_FUNC_TABLE_OFFSET 0x14 +#if PICO_RP2040 +#define BOOTROM_DATA_TABLE_OFFSET 0x16 +#endif + +#if PICO_RP2040 +#define BOOTROM_VTABLE_OFFSET 0x00 +#define BOOTROM_TABLE_LOOKUP_OFFSET 0x18 +#else +#define BOOTROM_WELL_KNOWN_PTR_SIZE 2 +#if defined(__riscv) +#define BOOTROM_ENTRY_OFFSET 0x7dfc +#define BOOTROM_TABLE_LOOKUP_ENTRY_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE) +#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE*2) +#else +#define BOOTROM_VTABLE_OFFSET 0x00 +#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_FUNC_TABLE_OFFSET + BOOTROM_WELL_KNOWN_PTR_SIZE) +#endif +#endif + +#if !PICO_RP2040 || PICO_COMBINED_DOCS + +#define BOOTROM_OK 0 +//#define BOOTROM_ERROR_TIMEOUT (-1) +//#define BOOTROM_ERROR_GENERIC (-2) +//#define BOOTROM_ERROR_NO_DATA (-3) // E.g. read from an empty buffer/FIFO +#define BOOTROM_ERROR_NOT_PERMITTED (-4) // Permission violation e.g. write to read-only flash partition +#define BOOTROM_ERROR_INVALID_ARG (-5) // Argument is outside of range of supported values` +//#define BOOTROM_ERROR_IO (-6) +//#define BOOTROM_ERROR_BADAUTH (-7) +//#define BOOTROM_ERROR_CONNECT_FAILED (-8) +//#define BOOTROM_ERROR_INSUFFICIENT_RESOURCES (-9) // Dynamic allocation of resources failed +#define BOOTROM_ERROR_INVALID_ADDRESS (-10) // Address argument was out-of-bounds or was determined to be an address that the caller may not access +#define BOOTROM_ERROR_BAD_ALIGNMENT (-11) // Address modulo transfer chunk size was nonzero (e.g. word-aligned transfer with address % 4 != 0) +#define BOOTROM_ERROR_INVALID_STATE (-12) // Something happened or failed to happen in the past, and consequently we (currently) can't service the request +#define BOOTROM_ERROR_BUFFER_TOO_SMALL (-13) // A user-allocated buffer was too small to hold the result or working state of this function +#define BOOTROM_ERROR_PRECONDITION_NOT_MET (-14) // This call failed because another ROM function must be called first +#define BOOTROM_ERROR_MODIFIED_DATA (-15) // Cached data was determined to be inconsistent with the full version of the data it was calculated from +#define BOOTROM_ERROR_INVALID_DATA (-16) // A data structure failed to validate +#define BOOTROM_ERROR_NOT_FOUND (-17) // Attempted to access something that does not exist; or, a search failed +#define BOOTROM_ERROR_UNSUPPORTED_MODIFICATION (-18) // Write is impossible based on previous writes; e.g. attempted to clear an OTP bit +#define BOOTROM_ERROR_LOCK_REQUIRED (-19) // A required lock is not owned +#define BOOTROM_ERROR_LAST (-19) + +#define RT_FLAG_FUNC_RISCV 0x0001 +#define RT_FLAG_FUNC_RISCV_FAR 0x0003 +#define RT_FLAG_FUNC_ARM_SEC 0x0004 +// reserved for 32-bit pointer: 0x0008 +#define RT_FLAG_FUNC_ARM_NONSEC 0x0010 +// reserved for 32-bit pointer: 0x0020 +#define RT_FLAG_DATA 0x0040 +// reserved for 32-bit pointer: 0x0080 + +#define PARTITION_TABLE_MAX_PARTITIONS 16 +// note this is deliberately > MAX_PARTITIONs is likely to be, and also -1 as a signed byte +#define PARTITION_TABLE_NO_PARTITION_INDEX 0xff + +// todo these are duplicated in picoboot_constants.h +// values 0-7 are secure/non-secure +#define BOOT_TYPE_NORMAL 0 +#define BOOT_TYPE_BOOTSEL 2 +#define BOOT_TYPE_RAM_IMAGE 3 +#define BOOT_TYPE_FLASH_UPDATE 4 + +// values 8-15 are secure only +#define BOOT_TYPE_PC_SP 0xd + +// ORed in if a bootloader chained into the image +#define BOOT_TYPE_CHAINED_FLAG 0x80 + +// call from NS to S +#ifndef __riscv +#define BOOTROM_API_CALLBACK_secure_call 0 +#endif +#define BOOTROM_API_CALLBACK_COUNT 1 + +#define BOOTROM_LOCK_SHA_256 0 +#define BOOTROM_LOCK_FLASH_OP 1 +#define BOOTROM_LOCK_OTP 2 +#define BOOTROM_LOCK_MAX 2 + +#define BOOTROM_LOCK_ENABLE 7 + +#define BOOT_PARTITION_NONE (-1) +#define BOOT_PARTITION_SLOT0 (-2) +#define BOOT_PARTITION_SLOT1 (-3) +#define BOOT_PARTITION_WINDOW (-4) + +#define BOOT_DIAGNOSTIC_WINDOW_SEARCHED 0x01 +// note if both BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP and BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP then the block loop was valid +// but it has a PARTITION_TABLE which while it passed the initial verification (and hash/sig) had invalid contents +// (discovered when it was later loaded) +#define BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP 0x02 +#define BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP 0x04 +#define BOOT_DIAGNOSTIC_VALID_IMAGE_DEF 0x08 +#define BOOT_DIAGNOSTIC_HAS_PARTITION_TABLE 0x10 +#define BOOT_DIAGNOSTIC_CONSIDERED 0x20 +#define BOOT_DIAGNOSTIC_CHOSEN 0x40 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_LSB 7 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_MATCHING_KEY_FOR_VERIFY 0x80 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_HASH_FOR_VERIFY 0x100 +#define BOOT_DIAGNOSTIC_PARTITION_TABLE_VERIFIED_OK 0x200 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_LSB 10 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_MATCHING_KEY_FOR_VERIFY 0x400 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_HASH_FOR_VERIFY 0x800 +#define BOOT_DIAGNOSTIC_IMAGE_DEF_VERIFIED_OK 0x1000 + +#define BOOT_DIAGNOSTIC_LOAD_MAP_ENTRIES_LOADED 0x2000 +#define BOOT_DIAGNOSTIC_IMAGE_LAUNCHED 0x4000 +#define BOOT_DIAGNOSTIC_IMAGE_CONDITION_FAILURE 0x8000 + +#define BOOT_PARSED_BLOCK_DIAGNOSTIC_MATCHING_KEY_FOR_VERIFY 0x1 // if this is present and VERIFIED_OK isn't the sig check failed +#define BOOT_PARSED_BLOCK_DIAGNOSTIC_HASH_FOR_VERIFY 0x2 // if this is present and VERIFIED_OL isn't then hash check failed +#define BOOT_PARSED_BLOCK_DIAGNOSTIC_VERIFIED_OK 0x4 + +#define BOOT_TBYB_AND_UPDATE_FLAG_BUY_PENDING 0x1 +#define BOOT_TBYB_AND_UPDATE_FLAG_OTP_VERSION_APPLIED 0x2 +#define BOOT_TBYB_AND_UPDATE_FLAG_OTHER_ERASED 0x4 + +#ifndef __ASSEMBLER__ +// Limited to 3 arguments in case of varm multiplex hint (trashes Arm r3) +typedef int (*bootrom_api_callback_generic_t)(uint32_t r0, uint32_t r1, uint32_t r2); +// Return negative for error, else number of bytes transferred: +//typedef int (*bootrom_api_callback_stdout_put_blocking_t)(const uint8_t *buffer, uint32_t size); +//typedef int (*bootrom_api_callback_stdin_get_t)(uint8_t *buffer, uint32_t size); +//typedef void (*bootrom_api_callback_core1_security_setup_t)(void); +#endif + +#endif + +/*! \brief Return a bootrom lookup code based on two ASCII characters + * \ingroup pico_bootrom + * + * These codes are uses to lookup data or function addresses in the bootrom + * + * \param c1 the first character + * \param c2 the second character + * \return the 'code' to use in rom_func_lookup() or rom_data_lookup() + */ +#define ROM_TABLE_CODE(c1, c2) ((c1) | ((c2) << 8)) + +// ROM FUNCTIONS + +// RP2040 & RP2350 +#define ROM_DATA_SOFTWARE_GIT_REVISION ROM_TABLE_CODE('G', 'R') +#define ROM_FUNC_FLASH_ENTER_CMD_XIP ROM_TABLE_CODE('C', 'X') +#define ROM_FUNC_FLASH_EXIT_XIP ROM_TABLE_CODE('E', 'X') +#define ROM_FUNC_FLASH_FLUSH_CACHE ROM_TABLE_CODE('F', 'C') +#define ROM_FUNC_CONNECT_INTERNAL_FLASH ROM_TABLE_CODE('I', 'F') +#define ROM_FUNC_FLASH_RANGE_ERASE ROM_TABLE_CODE('R', 'E') +#define ROM_FUNC_FLASH_RANGE_PROGRAM ROM_TABLE_CODE('R', 'P') + + +#if PICO_RP2040 +// RP2040 only +#define ROM_FUNC_MEMCPY44 ROM_TABLE_CODE('C', '4') +#define ROM_DATA_COPYRIGHT ROM_TABLE_CODE('C', 'R') +#define ROM_FUNC_CLZ32 ROM_TABLE_CODE('L', '3') +#define ROM_FUNC_MEMCPY ROM_TABLE_CODE('M', 'C') +#define ROM_FUNC_MEMSET ROM_TABLE_CODE('M', 'S') +#define ROM_FUNC_POPCOUNT32 ROM_TABLE_CODE('P', '3') +#define ROM_FUNC_REVERSE32 ROM_TABLE_CODE('R', '3') +#define ROM_FUNC_MEMSET4 ROM_TABLE_CODE('S', '4') +#define ROM_FUNC_CTZ32 ROM_TABLE_CODE('T', '3') +#define ROM_FUNC_RESET_USB_BOOT ROM_TABLE_CODE('U', 'B') +#endif + +#if !PICO_RP2040 || PICO_COMBINED_DOCS +// RP2350 only +#define ROM_FUNC_PICK_AB_PARTITION ROM_TABLE_CODE('A', 'B') +#define ROM_FUNC_CHAIN_IMAGE ROM_TABLE_CODE('C', 'I') +#define ROM_FUNC_EXPLICIT_BUY ROM_TABLE_CODE('E', 'B') +#define ROM_FUNC_FLASH_RUNTIME_TO_STORAGE_ADDR ROM_TABLE_CODE('F', 'A') +#define ROM_DATA_FLASH_DEVINFO16_PTR ROM_TABLE_CODE('F', 'D') +#define ROM_FUNC_FLASH_OP ROM_TABLE_CODE('F', 'O') +#define ROM_FUNC_GET_B_PARTITION ROM_TABLE_CODE('G', 'B') +#define ROM_FUNC_GET_PARTITION_TABLE_INFO ROM_TABLE_CODE('G', 'P') +#define ROM_FUNC_GET_SYS_INFO ROM_TABLE_CODE('G', 'S') +#define ROM_FUNC_GET_UF2_TARGET_PARTITION ROM_TABLE_CODE('G', 'U') +#define ROM_FUNC_LOAD_PARTITION_TABLE ROM_TABLE_CODE('L', 'P') +#define ROM_FUNC_OTP_ACCESS ROM_TABLE_CODE('O', 'A') +#define ROM_DATA_PARTITION_TABLE_PTR ROM_TABLE_CODE('P', 'T') +#define ROM_FUNC_FLASH_RESET_ADDRESS_TRANS ROM_TABLE_CODE('R', 'A') +#define ROM_FUNC_REBOOT ROM_TABLE_CODE('R', 'B') +#define ROM_FUNC_SET_ROM_CALLBACK ROM_TABLE_CODE('R', 'C') +#define ROM_FUNC_SECURE_CALL ROM_TABLE_CODE('S', 'C') +#define ROM_FUNC_SET_NS_API_PERMISSION ROM_TABLE_CODE('S', 'P') +#define ROM_FUNC_BOOTROM_STATE_RESET ROM_TABLE_CODE('S', 'R') +#define ROM_FUNC_SET_BOOTROM_STACK ROM_TABLE_CODE('S', 'S') +#define ROM_DATA_SAVED_XIP_SETUP_FUNC_PTR ROM_TABLE_CODE('X', 'F') +#define ROM_FUNC_FLASH_SELECT_XIP_READ_MODE ROM_TABLE_CODE('X', 'M') +#define ROM_FUNC_VALIDATE_NS_BUFFER ROM_TABLE_CODE('V', 'B') +#endif + +// these form a bit set +#define BOOTROM_STATE_RESET_CURRENT_CORE 0x01 +#define BOOTROM_STATE_RESET_OTHER_CORE 0x02 +#define BOOTROM_STATE_RESET_GLOBAL_STATE 0x04 // reset any global state (e.g. permissions) + +// partition level stuff is returned first (note PT_INFO flags is only 16 bits) + +// 3 words: pt_count, unpartitioned_perm_loc, unpartioned_perm_flags +#define PT_INFO_PT_INFO 0x0001 +#define PT_INFO_SINGLE_PARTITION 0x8000 // marker to just include a single partition in the results) + +// then in order per partition selected + +// 2 words: unpartitioned_perm_loc, unpartioned_perm_flags +#define PT_INFO_PARTITION_LOCATION_AND_FLAGS 0x0010 +// 2 words: id lsb first +#define PT_INFO_PARTITION_ID 0x0020 +// n+1 words: n, family_id... +#define PT_INFO_PARTITION_FAMILY_IDS 0x0040 +// (n+3)/4 words... bytes are: n (len), c0, c1, ... cn-1 padded to word boundary with zeroes +#define PT_INFO_PARTITION_NAME 0x0080 + +// items are returned in order +// 3 words package_id, device_id_lo, device_id_hi +#define SYS_INFO_CHIP_INFO 0x0001 +// 1 word: chip specific critical bits +#define SYS_INFO_CRITICAL 0x0002 +// 1 word: bytes: cpu_type, supported_cpu_type_bitfield +#define SYS_INFO_CPU_INFO 0x0004 +// 1 word: same as FLASH_DEVINFO row in OTP +#define SYS_INFO_FLASH_DEV_INFO 0x0008 +// 4 words +#define SYS_INFO_BOOT_RANDOM 0x0010 +// 2 words lsb first +#define SYS_INFO_NONCE 0x0020 +// 4 words boot_info, boot_diagnostic, boot_param0, boot_param1 +#define SYS_INFO_BOOT_INFO 0x0040 + +#define BOOTROM_NS_API_get_sys_info 0 +#define BOOTROM_NS_API_checked_flash_op 1 +#define BOOTROM_NS_API_flash_runtime_to_storage_addr 2 +#define BOOTROM_NS_API_get_partition_table_info 3 +#define BOOTROM_NS_API_secure_call 4 +#define BOOTROM_NS_API_otp_access 5 +#define BOOTROM_NS_API_reboot 6 +#define BOOTROM_NS_API_get_b_partition 7 +#define BOOTROM_NS_API_COUNT 8 + +#define OTP_CMD_ROW_BITS _u(0x0000ffff) +#define OTP_CMD_ROW_LSB _u(0) +#define OTP_CMD_WRITE_BITS _u(0x00010000) +#define OTP_CMD_WRITE_LSB _u(16) +#define OTP_CMD_ECC_BITS _u(0x00020000) +#define OTP_CMD_ECC_LSB _u(17) + +#ifndef __ASSEMBLER__ +static_assert(OTP_CMD_WRITE_BITS == (1 << OTP_CMD_WRITE_LSB), ""); +static_assert(OTP_CMD_ECC_BITS == (1 << OTP_CMD_ECC_LSB), ""); + +typedef struct { + uint32_t permissions_and_location; + uint32_t permissions_and_flags; +} resident_partition_t; +static_assert(sizeof(resident_partition_t) == 8, ""); + +typedef struct otp_cmd { + uint32_t flags; +} otp_cmd_t; + +typedef enum { + BOOTROM_XIP_MODE_03H_SERIAL = 0, + BOOTROM_XIP_MODE_0BH_SERIAL, + BOOTROM_XIP_MODE_BBH_DUAL, + BOOTROM_XIP_MODE_EBH_QUAD, + BOOTROM_XIP_MODE_N_MODES +} bootrom_xip_mode_t; + +// The checked flash API wraps the low-level flash routines from generic_flash, adding bounds +// checking, permission checking against the resident partition table, and simple address +// translation. The low-level API deals with flash offsets (i.e. distance from the start of the +// first flash device, measured in bytes) but the checked flash API accepts one of two types of +// address: +// +// - Flash runtime addresses: the address of some flash-resident data or code in the currently +// running image. The flash addresses your binary is "linked at" by the linker. +// - Flash storage addresses: a flash offset, plus the address base where QSPI hardware is first +// mapped on the system bus (XIP_BASE constant from addressmap.h) +// +// These addresses are one and the same *if* the currently running program is stored at the +// beginning of flash. They are different if the start of your image has been "rolled" by the flash +// boot path to make it appear at the address it was linked at even though it is stored at a +// different location in flash, which is necessary when you have A/B images for example. +// +// The address translation between flash runtime and flash storage addresses is configured in +// hardware by the QMI_ATRANSx registers, and this API assumes those registers contain a valid +// address mapping which it can use to translate runtime to storage addresses. + +typedef struct cflash_flags { + uint32_t flags; +} cflash_flags_t; + +#endif // #ifdef __ASSEMBLER__ + +// Bits which are permitted to be set in a flags variable -- any other bits being set is an error +#define CFLASH_FLAGS_BITS 0x00070301u + +// Used to tell checked flash API which space a given address belongs to +#define CFLASH_ASPACE_BITS 0x00000001u +#define CFLASH_ASPACE_LSB _u(0) +#define CFLASH_ASPACE_VALUE_STORAGE _u(0) +#define CFLASH_ASPACE_VALUE_RUNTIME _u(1) + +// Used to tell checked flash APIs the effective security level of a flash access (may be forced to +// one of these values for the NonSecure-exported version of this API) +#define CFLASH_SECLEVEL_BITS 0x00000300u +#define CFLASH_SECLEVEL_LSB _u(8) +// Zero is not a valid security level: +#define CFLASH_SECLEVEL_VALUE_SECURE _u(1) +#define CFLASH_SECLEVEL_VALUE_NONSECURE _u(2) +#define CFLASH_SECLEVEL_VALUE_BOOTLOADER _u(3) + +#define CFLASH_OP_BITS 0x00070000u +#define CFLASH_OP_LSB _u(16) +// Erase size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a +// multiple of 4096 bytes (one flash sector). +#define CFLASH_OP_VALUE_ERASE _u(0) +// Program size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a +// multiple of 256 bytes (one flash page). +#define CFLASH_OP_VALUE_PROGRAM _u(1) +// Read size_bytes bytes of flash, starting at address addr. There are no alignment restrictions on +// addr or size_bytes. +#define CFLASH_OP_VALUE_READ _u(2) +#define CFLASH_OP_MAX _u(2) + +#endif diff --git a/lib/pico-sdk/boot/picoboot_constants.h b/lib/pico-sdk/boot/picoboot_constants.h index ac78ea213..ffb3b8cc4 100644 --- a/lib/pico-sdk/boot/picoboot_constants.h +++ b/lib/pico-sdk/boot/picoboot_constants.h @@ -9,11 +9,11 @@ #define REBOOT2_TYPE_MASK 0x0f -// note these match REBOOT_TYPE in pico/bootrom_constants.h (also 0 is used for PC_SP for backwards compatibility with RP2040) +// note these match REBOOT_TYPE in pico/bootrom_constants.h // values 0-7 are secure/non-secure #define REBOOT2_FLAG_REBOOT_TYPE_NORMAL 0x0 // param0 = diagnostic partition -#define REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL 0x2 // param0 = bootsel_flags, param1 = gpio_config -#define REBOOT2_FLAG_REBOOT_TYPE_RAM_IMAGE 0x3 // param0 = image_base, param1 = image_end +#define REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL 0x2 // param0 = gpio_pin_number, param1 = flags +#define REBOOT2_FLAG_REBOOT_TYPE_RAM_IMAGE 0x3 // param0 = image_region_base, param1 = image_region_size #define REBOOT2_FLAG_REBOOT_TYPE_FLASH_UPDATE 0x4 // param0 = update_base // values 8-15 are secure only @@ -39,4 +39,4 @@ #define UF2_STATUS_ABORT_BAD_ADDRESS 0x20 #define UF2_STATUS_ABORT_WRITE_ERROR 0x40 #define UF2_STATUS_ABORT_REBOOT_FAILED 0x80 -#endif \ No newline at end of file +#endif diff --git a/lib/pico-sdk/boot/uf2.h b/lib/pico-sdk/boot/uf2.h index 271540a20..279d4a131 100644 --- a/lib/pico-sdk/boot/uf2.h +++ b/lib/pico-sdk/boot/uf2.h @@ -20,19 +20,30 @@ #define UF2_MAGIC_START1 0x9E5D5157u #define UF2_MAGIC_END 0x0AB16F30u -#define UF2_FLAG_NOT_MAIN_FLASH 0x00000001u -#define UF2_FLAG_FILE_CONTAINER 0x00001000u -#define UF2_FLAG_FAMILY_ID_PRESENT 0x00002000u -#define UF2_FLAG_MD5_PRESENT 0x00004000u +#define UF2_FLAG_NOT_MAIN_FLASH 0x00000001u +#define UF2_FLAG_FILE_CONTAINER 0x00001000u +#define UF2_FLAG_FAMILY_ID_PRESENT 0x00002000u +#define UF2_FLAG_MD5_PRESENT 0x00004000u +#define UF2_FLAG_EXTENSION_FLAGS_PRESENT 0x00008000u +// Extra family IDs +#define CYW43_FIRMWARE_FAMILY_ID 0xe48bff55u + +// Bootrom supported family IDs #define RP2040_FAMILY_ID 0xe48bff56u #define ABSOLUTE_FAMILY_ID 0xe48bff57u #define DATA_FAMILY_ID 0xe48bff58u #define RP2350_ARM_S_FAMILY_ID 0xe48bff59u #define RP2350_RISCV_FAMILY_ID 0xe48bff5au #define RP2350_ARM_NS_FAMILY_ID 0xe48bff5bu -#define FAMILY_ID_MAX 0xe48bff5bu +#define BOOTROM_FAMILY_ID_MIN RP2040_FAMILY_ID +#define BOOTROM_FAMILY_ID_MAX RP2350_ARM_NS_FAMILY_ID +// Defined for backwards compatibility +#define FAMILY_ID_MAX BOOTROM_FAMILY_ID_MAX + +// 04 e3 57 99 +#define UF2_EXTENSION_RP2_IGNORE_BLOCK 0x9957e304 struct uf2_block { // 32 byte header diff --git a/lib/pico-sdk/hardware/platform_defs.h b/lib/pico-sdk/hardware/platform_defs.h index 924336a92..c1a6f5f44 100644 --- a/lib/pico-sdk/hardware/platform_defs.h +++ b/lib/pico-sdk/hardware/platform_defs.h @@ -15,6 +15,9 @@ #define NUM_ALARMS 4u #define NUM_IRQS 32u +#define NUM_USER_IRQS 6u +#define FIRST_USER_IRQ (NUM_IRQS - NUM_USER_IRQS) +#define VTABLE_FIRST_IRQ 16 #define NUM_SPIN_LOCKS 32u diff --git a/lib/pico-sdk/pico-sdk.patch b/lib/pico-sdk/pico-sdk.patch index 0a27b4605..91b5dfd9c 100644 --- a/lib/pico-sdk/pico-sdk.patch +++ b/lib/pico-sdk/pico-sdk.patch @@ -1,3 +1,29 @@ +This file summarizes the local changes from the upstream pico-sdk +repository (version 2.2.0). In brief, the following steps can be used +to recreate the code here from the main pico-sdk code: + +cp /pico-sdk/src/common/boot_uf2_headers/include/boot/*.h boot/ +cp /pico-sdk/src/common/boot_picoboot_headers/include/boot/*.h boot/ +cp /pico-sdk/src/rp2_common/boot_bootrom_headers/include/boot/*.h boot/ + +cp /pico-sdk/src/rp2_common/hardware_base/include/hardware/*.h hardware/ +cp /pico-sdk/src/host/pico_platform/include/hardware/*.h hardware/ + +cp /pico-sdk/src/rp2_common/pico_bootrom/include/pico/bootrom_constants.h pico/ +cp /pico-sdk/src/host/pico_platform/include/pico/*.h pico/ + +cp -a /pico-sdk/src/rp2040/boot_stage2/ rp2040/ +cp /pico-sdk/src/rp2_common/cmsis/stub/CMSIS/Device/RP2040/Include/*.h rp2040/cmsis_include/ +cp -a /pico-sdk/src/rp2040/hardware_regs/include/hardware rp2040/ +cp -a /pico-sdk/src/rp2040/hardware_structs/include/hardware rp2040/ +cp /pico-sdk/src/rp2040/pico_platform/include/pico/*.S rp2040/pico/ + +cp /pico-sdk/src/rp2_common/cmsis/stub/CMSIS/Device/RP2350/Include/*.h rp2350/cmsis_include/ +cp /pico-sdk/src/rp2350/hardware_regs/include/hardware/regs/*.h rp2350/hardware/regs/ +cp /pico-sdk/src/rp2350/hardware_structs/include/hardware/structs/*.h rp2350/hardware/structs/ + +patch -p3 < pico-sdk.patch + diff --git a/lib/pico-sdk/hardware/address_mapped.h b/lib/pico-sdk/hardware/address_mapped.h index b384f5572..635a275b5 100644 --- a/lib/pico-sdk/hardware/address_mapped.h diff --git a/lib/pico-sdk/pico/bootrom_constants.h b/lib/pico-sdk/pico/bootrom_constants.h index 924487f8c..b3bfd47ea 100644 --- a/lib/pico-sdk/pico/bootrom_constants.h +++ b/lib/pico-sdk/pico/bootrom_constants.h @@ -4,339 +4,5 @@ * SPDX-License-Identifier: BSD-3-Clause */ -#ifndef _PICO_BOOTROM_CONSTANTS_H -#define _PICO_BOOTROM_CONSTANTS_H - -#ifndef NO_PICO_PLATFORM -#include "pico/platform.h" -#endif - -// ROOT ADDRESSES -#define BOOTROM_MAGIC_OFFSET 0x10 -#define BOOTROM_FUNC_TABLE_OFFSET 0x14 -#if PICO_RP2040 -#define BOOTROM_DATA_TABLE_OFFSET 0x16 -#endif - -#if PICO_RP2040 -#define BOOTROM_VTABLE_OFFSET 0x00 -#define BOOTROM_TABLE_LOOKUP_OFFSET 0x18 -#else -// todo remove this (or #ifdef it for A1/A2) -#define BOOTROM_IS_A2() ((*(volatile uint8_t *)0x13) == 2) -#define BOOTROM_WELL_KNOWN_PTR_SIZE (BOOTROM_IS_A2() ? 2 : 4) -#if defined(__riscv) -#define BOOTROM_ENTRY_OFFSET 0x7dfc -#define BOOTROM_TABLE_LOOKUP_ENTRY_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE) -#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_ENTRY_OFFSET - BOOTROM_WELL_KNOWN_PTR_SIZE*2) -#else -#define BOOTROM_VTABLE_OFFSET 0x00 -#define BOOTROM_TABLE_LOOKUP_OFFSET (BOOTROM_FUNC_TABLE_OFFSET + BOOTROM_WELL_KNOWN_PTR_SIZE) -#endif -#endif - -#if !PICO_RP2040 || PICO_COMBINED_DOCS - -#define BOOTROM_OK 0 -//#define BOOTROM_ERROR_TIMEOUT (-1) -//#define BOOTROM_ERROR_GENERIC (-2) -//#define BOOTROM_ERROR_NO_DATA (-3) // E.g. read from an empty buffer/FIFO -#define BOOTROM_ERROR_NOT_PERMITTED (-4) // Permission violation e.g. write to read-only flash partition -#define BOOTROM_ERROR_INVALID_ARG (-5) // Argument is outside of range of supported values` -//#define BOOTROM_ERROR_IO (-6) -//#define BOOTROM_ERROR_BADAUTH (-7) -//#define BOOTROM_ERROR_CONNECT_FAILED (-8) -//#define BOOTROM_ERROR_INSUFFICIENT_RESOURCES (-9) // Dynamic allocation of resources failed -#define BOOTROM_ERROR_INVALID_ADDRESS (-10) // Address argument was out-of-bounds or was determined to be an address that the caller may not access -#define BOOTROM_ERROR_BAD_ALIGNMENT (-11) // Address modulo transfer chunk size was nonzero (e.g. word-aligned transfer with address % 4 != 0) -#define BOOTROM_ERROR_INVALID_STATE (-12) // Something happened or failed to happen in the past, and consequently we (currently) can't service the request -#define BOOTROM_ERROR_BUFFER_TOO_SMALL (-13) // A user-allocated buffer was too small to hold the result or working state of this function -#define BOOTROM_ERROR_PRECONDITION_NOT_MET (-14) // This call failed because another ROM function must be called first -#define BOOTROM_ERROR_MODIFIED_DATA (-15) // Cached data was determined to be inconsistent with the full version of the data it was calculated from -#define BOOTROM_ERROR_INVALID_DATA (-16) // A data structure failed to validate -#define BOOTROM_ERROR_NOT_FOUND (-17) // Attempted to access something that does not exist; or, a search failed -#define BOOTROM_ERROR_UNSUPPORTED_MODIFICATION (-18) // Write is impossible based on previous writes; e.g. attempted to clear an OTP bit -#define BOOTROM_ERROR_LOCK_REQUIRED (-19) // A required lock is not owned -#define BOOTROM_ERROR_LAST (-19) - -#define RT_FLAG_FUNC_RISCV 0x0001 -#define RT_FLAG_FUNC_RISCV_FAR 0x0003 -#define RT_FLAG_FUNC_ARM_SEC 0x0004 -// reserved for 32-bit pointer: 0x0008 -#define RT_FLAG_FUNC_ARM_NONSEC 0x0010 -// reserved for 32-bit pointer: 0x0020 -#define RT_FLAG_DATA 0x0040 -// reserved for 32-bit pointer: 0x0080 - -#define PARTITION_TABLE_MAX_PARTITIONS 16 -// note this is deliberately > MAX_PARTITIONs is likely to be, and also -1 as a signed byte -#define PARTITION_TABLE_NO_PARTITION_INDEX 0xff - -// todo these are duplicated in picoboot_constants.h -// values 0-7 are secure/non-secure -#define BOOT_TYPE_NORMAL 0 -#define BOOT_TYPE_BOOTSEL 2 -#define BOOT_TYPE_RAM_IMAGE 3 -#define BOOT_TYPE_FLASH_UPDATE 4 - -// values 8-15 are secure only -#define BOOT_TYPE_PC_SP 0xd - -// ORed in if a bootloader chained into the image -#define BOOT_TYPE_CHAINED_FLAG 0x80 - -// call from NS to S -#ifndef __riscv -#define BOOTROM_API_CALLBACK_secure_call 0 -#endif -#define BOOTROM_API_CALLBACK_COUNT 1 - -#define BOOTROM_LOCK_SHA_256 0 -#define BOOTROM_LOCK_FLASH_OP 1 -#define BOOTROM_LOCK_OTP 2 -#define BOOTROM_LOCK_MAX 2 - -#define BOOTROM_LOCK_ENABLE 7 - -#define BOOT_PARTITION_NONE (-1) -#define BOOT_PARTITION_SLOT0 (-2) -#define BOOT_PARTITION_SLOT1 (-3) -#define BOOT_PARTITION_WINDOW (-4) - -#define BOOT_DIAGNOSTIC_WINDOW_SEARCHED 0x01 -// note if both BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP and BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP then the block loop was valid -// but it has a PARTITION_TABLE which while it passed the initial verification (and hash/sig) had invalid contents -// (discovered when it was later loaded) -#define BOOT_DIAGNOSTIC_INVALID_BLOCK_LOOP 0x02 -#define BOOT_DIAGNOSTIC_VALID_BLOCK_LOOP 0x04 -#define BOOT_DIAGNOSTIC_VALID_IMAGE_DEF 0x08 -#define BOOT_DIAGNOSTIC_HAS_PARTITION_TABLE 0x10 -#define BOOT_DIAGNOSTIC_CONSIDERED 0x20 -#define BOOT_DIAGNOSTIC_CHOSEN 0x40 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_LSB 7 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_MATCHING_KEY_FOR_VERIFY 0x80 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_HASH_FOR_VERIFY 0x100 -#define BOOT_DIAGNOSTIC_PARTITION_TABLE_VERIFIED_OK 0x200 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_LSB 10 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_MATCHING_KEY_FOR_VERIFY 0x400 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_HASH_FOR_VERIFY 0x800 -#define BOOT_DIAGNOSTIC_IMAGE_DEF_VERIFIED_OK 0x1000 - -#define BOOT_DIAGNOSTIC_LOAD_MAP_ENTRIES_LOADED 0x2000 -#define BOOT_DIAGNOSTIC_IMAGE_LAUNCHED 0x4000 -#define BOOT_DIAGNOSTIC_IMAGE_CONDITION_FAILURE 0x8000 - -#define BOOT_PARSED_BLOCK_DIAGNOSTIC_MATCHING_KEY_FOR_VERIFY 0x1 // if this is present and VERIFIED_OK isn't the sig check failed -#define BOOT_PARSED_BLOCK_DIAGNOSTIC_HASH_FOR_VERIFY 0x2 // if this is present and VERIFIED_OL isn't then hash check failed -#define BOOT_PARSED_BLOCK_DIAGNOSTIC_VERIFIED_OK 0x4 - -#define BOOT_TBYB_AND_UPDATE_FLAG_BUY_PENDING 0x1 -#define BOOT_TBYB_AND_UPDATE_FLAG_OTP_VERSION_APPLIED 0x2 -#define BOOT_TBYB_AND_UPDATE_FLAG_OTHER_ERASED 0x4 - -#ifndef __ASSEMBLER__ -// Limited to 3 arguments in case of varm multiplex hint (trashes Arm r3) -typedef int (*bootrom_api_callback_generic_t)(uint32_t r0, uint32_t r1, uint32_t r2); -// Return negative for error, else number of bytes transferred: -//typedef int (*bootrom_api_callback_stdout_put_blocking_t)(const uint8_t *buffer, uint32_t size); -//typedef int (*bootrom_api_callback_stdin_get_t)(uint8_t *buffer, uint32_t size); -//typedef void (*bootrom_api_callback_core1_security_setup_t)(void); -#endif - -#endif - -/*! \brief Return a bootrom lookup code based on two ASCII characters - * \ingroup pico_bootrom - * - * These codes are uses to lookup data or function addresses in the bootrom - * - * \param c1 the first character - * \param c2 the second character - * \return the 'code' to use in rom_func_lookup() or rom_data_lookup() - */ -#define ROM_TABLE_CODE(c1, c2) ((c1) | ((c2) << 8)) - -// ROM FUNCTIONS - -// RP2040 & RP2350 -#define ROM_DATA_SOFTWARE_GIT_REVISION ROM_TABLE_CODE('G', 'R') -#define ROM_FUNC_FLASH_ENTER_CMD_XIP ROM_TABLE_CODE('C', 'X') -#define ROM_FUNC_FLASH_EXIT_XIP ROM_TABLE_CODE('E', 'X') -#define ROM_FUNC_FLASH_FLUSH_CACHE ROM_TABLE_CODE('F', 'C') -#define ROM_FUNC_CONNECT_INTERNAL_FLASH ROM_TABLE_CODE('I', 'F') -#define ROM_FUNC_FLASH_RANGE_ERASE ROM_TABLE_CODE('R', 'E') -#define ROM_FUNC_FLASH_RANGE_PROGRAM ROM_TABLE_CODE('R', 'P') - - -#if PICO_RP2040 -// RP2040 only -#define ROM_FUNC_MEMCPY44 ROM_TABLE_CODE('C', '4') -#define ROM_DATA_COPYRIGHT ROM_TABLE_CODE('C', 'R') -#define ROM_FUNC_CLZ32 ROM_TABLE_CODE('L', '3') -#define ROM_FUNC_MEMCPY ROM_TABLE_CODE('M', 'C') -#define ROM_FUNC_MEMSET ROM_TABLE_CODE('M', 'S') -#define ROM_FUNC_POPCOUNT32 ROM_TABLE_CODE('P', '3') -#define ROM_FUNC_REVERSE32 ROM_TABLE_CODE('R', '3') -#define ROM_FUNC_MEMSET4 ROM_TABLE_CODE('S', '4') -#define ROM_FUNC_CTZ32 ROM_TABLE_CODE('T', '3') -#define ROM_FUNC_RESET_USB_BOOT ROM_TABLE_CODE('U', 'B') -#endif - -#if !PICO_RP2040 || PICO_COMBINED_DOCS -// RP2350 only -#define ROM_FUNC_PICK_AB_PARTITION ROM_TABLE_CODE('A', 'B') -#define ROM_FUNC_CHAIN_IMAGE ROM_TABLE_CODE('C', 'I') -#define ROM_FUNC_EXPLICIT_BUY ROM_TABLE_CODE('E', 'B') -#define ROM_FUNC_FLASH_RUNTIME_TO_STORAGE_ADDR ROM_TABLE_CODE('F', 'A') -#define ROM_DATA_FLASH_DEVINFO16_PTR ROM_TABLE_CODE('F', 'D') -#define ROM_FUNC_FLASH_OP ROM_TABLE_CODE('F', 'O') -#define ROM_FUNC_GET_B_PARTITION ROM_TABLE_CODE('G', 'B') -#define ROM_FUNC_GET_PARTITION_TABLE_INFO ROM_TABLE_CODE('G', 'P') -#define ROM_FUNC_GET_SYS_INFO ROM_TABLE_CODE('G', 'S') -#define ROM_FUNC_GET_UF2_TARGET_PARTITION ROM_TABLE_CODE('G', 'U') -#define ROM_FUNC_LOAD_PARTITION_TABLE ROM_TABLE_CODE('L', 'P') -#define ROM_FUNC_OTP_ACCESS ROM_TABLE_CODE('O', 'A') -#define ROM_DATA_PARTITION_TABLE_PTR ROM_TABLE_CODE('P', 'T') -#define ROM_FUNC_FLASH_RESET_ADDRESS_TRANS ROM_TABLE_CODE('R', 'A') -#define ROM_FUNC_REBOOT ROM_TABLE_CODE('R', 'B') -#define ROM_FUNC_SET_ROM_CALLBACK ROM_TABLE_CODE('R', 'C') -#define ROM_FUNC_SECURE_CALL ROM_TABLE_CODE('S', 'C') -#define ROM_FUNC_SET_NS_API_PERMISSION ROM_TABLE_CODE('S', 'P') -#define ROM_FUNC_BOOTROM_STATE_RESET ROM_TABLE_CODE('S', 'R') -#define ROM_FUNC_SET_BOOTROM_STACK ROM_TABLE_CODE('S', 'S') -#define ROM_DATA_SAVED_XIP_SETUP_FUNC_PTR ROM_TABLE_CODE('X', 'F') -#define ROM_FUNC_FLASH_SELECT_XIP_READ_MODE ROM_TABLE_CODE('X', 'M') -#define ROM_FUNC_VALIDATE_NS_BUFFER ROM_TABLE_CODE('V', 'B') -#endif - -// these form a bit set -#define BOOTROM_STATE_RESET_CURRENT_CORE 0x01 -#define BOOTROM_STATE_RESET_OTHER_CORE 0x02 -#define BOOTROM_STATE_RESET_GLOBAL_STATE 0x04 // reset any global state (e.g. permissions) - -// partition level stuff is returned first (note PT_INFO flags is only 16 bits) - -// 3 words: pt_count, unpartitioned_perm_loc, unpartioned_perm_flags -#define PT_INFO_PT_INFO 0x0001 -#define PT_INFO_SINGLE_PARTITION 0x8000 // marker to just include a single partition in the results) - -// then in order per partition selected - -// 2 words: unpartitioned_perm_loc, unpartioned_perm_flags -#define PT_INFO_PARTITION_LOCATION_AND_FLAGS 0x0010 -// 2 words: id lsb first -#define PT_INFO_PARTITION_ID 0x0020 -// n+1 words: n, family_id... -#define PT_INFO_PARTITION_FAMILY_IDS 0x0040 -// (n+3)/4 words... bytes are: n (len), c0, c1, ... cn-1 padded to word boundary with zeroes -#define PT_INFO_PARTITION_NAME 0x0080 - -// items are returned in order -// 3 words package_id, device_id, wafer_id -#define SYS_INFO_CHIP_INFO 0x0001 -// 1 word: chip specific critical bits -#define SYS_INFO_CRITICAL 0x0002 -// 1 word: bytes: cpu_type, supported_cpu_type_bitfield -#define SYS_INFO_CPU_INFO 0x0004 -// 1 word: same as FLASH_DEVINFO row in OTP -#define SYS_INFO_FLASH_DEV_INFO 0x0008 -// 4 words -#define SYS_INFO_BOOT_RANDOM 0x0010 -// 2 words lsb first -#define SYS_INFO_NONCE 0x0020 -// 4 words boot_info, boot_diagnostic, boot_param0, boot_param1 -#define SYS_INFO_BOOT_INFO 0x0040 - -#define BOOTROM_NS_API_get_sys_info 0 -#define BOOTROM_NS_API_checked_flash_op 1 -#define BOOTROM_NS_API_flash_runtime_to_storage_addr 2 -#define BOOTROM_NS_API_get_partition_table_info 3 -#define BOOTROM_NS_API_secure_call 4 -#define BOOTROM_NS_API_otp_access 5 -#define BOOTROM_NS_API_reboot 6 -#define BOOTROM_NS_API_get_b_partition 7 -#define BOOTROM_NS_API_COUNT 8 - -#ifndef __ASSEMBLER__ - -typedef struct { - uint32_t permissions_and_location; - uint32_t permissions_and_flags; -} resident_partition_t; -static_assert(sizeof(resident_partition_t) == 8, ""); - -#define OTP_CMD_ROW_BITS 0x0000ffffu -#define OTP_CMD_ROW_LSB 0u -#define OTP_CMD_WRITE_BITS 0x00010000u -#define OTP_CMD_ECC_BITS 0x00020000u - -typedef struct otp_cmd { - uint32_t flags; -} otp_cmd_t; - -typedef enum { - BOOTROM_XIP_MODE_03H_SERIAL = 0, - BOOTROM_XIP_MODE_0BH_SERIAL, - BOOTROM_XIP_MODE_BBH_DUAL, - BOOTROM_XIP_MODE_EBH_QUAD, - BOOTROM_XIP_MODE_N_MODES -} bootrom_xip_mode_t; - -// The checked flash API wraps the low-level flash routines from generic_flash, adding bounds -// checking, permission checking against the resident partition table, and simple address -// translation. The low-level API deals with flash offsets (i.e. distance from the start of the -// first flash device, measured in bytes) but the checked flash API accepts one of two types of -// address: -// -// - Flash runtime addresses: the address of some flash-resident data or code in the currently -// running image. The flash addresses your binary is "linked at" by the linker. -// - Flash storage addresses: a flash offset, plus the address base where QSPI hardware is first -// mapped on the system bus (XIP_BASE constant from addressmap.h) -// -// These addresses are one and the same *if* the currently running program is stored at the -// beginning of flash. They are different if the start of your image has been "rolled" by the flash -// boot path to make it appear at the address it was linked at even though it is stored at a -// different location in flash, which is necessary when you have A/B images for example. -// -// The address translation between flash runtime and flash storage addresses is configured in -// hardware by the QMI_ATRANSx registers, and this API assumes those registers contain a valid -// address mapping which it can use to translate runtime to storage addresses. - -typedef struct cflash_flags { - uint32_t flags; -} cflash_flags_t; - -// Bits which are permitted to be set in a flags variable -- any other bits being set is an error -#define CFLASH_FLAGS_BITS 0x00070301u - -// Used to tell checked flash API which space a given address belongs to -#define CFLASH_ASPACE_BITS 0x00000001u -#define CFLASH_ASPACE_LSB 0u -#define CFLASH_ASPACE_VALUE_STORAGE 0u -#define CFLASH_ASPACE_VALUE_RUNTIME 1u - -// Used to tell checked flash APIs the effective security level of a flash access (may be forced to -// one of these values for the NonSecure-exported version of this API) -#define CFLASH_SECLEVEL_BITS 0x00000300u -#define CFLASH_SECLEVEL_LSB 8u -// Zero is not a valid security level: -#define CFLASH_SECLEVEL_VALUE_SECURE 1u -#define CFLASH_SECLEVEL_VALUE_NONSECURE 2u -#define CFLASH_SECLEVEL_VALUE_BOOTLOADER 3u - -#define CFLASH_OP_BITS 0x00070000u -#define CFLASH_OP_LSB 16u -// Erase size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a -// multiple of 4096 bytes (one flash sector). -#define CFLASH_OP_VALUE_ERASE 0u -// Program size_bytes bytes of flash, starting at address addr. Both addr and size_bytes must be a -// multiple of 256 bytes (one flash page). -#define CFLASH_OP_VALUE_PROGRAM 1u -// Read size_bytes bytes of flash, starting at address addr. There are no alignment restrictions on -// addr or size_bytes. -#define CFLASH_OP_VALUE_READ 2u -#define CFLASH_OP_MAX 2u - -#endif - -#endif +// new location; this file kept for backwards compatibility +#include "boot/bootrom_constants.h" diff --git a/lib/pico-sdk/pico/platform.h b/lib/pico-sdk/pico/platform.h index dca69f265..0338354e5 100644 --- a/lib/pico-sdk/pico/platform.h +++ b/lib/pico-sdk/pico/platform.h @@ -11,7 +11,7 @@ #include #include -#ifdef __unix__ +#if defined __unix__ && defined __GLIBC__ #include @@ -47,7 +47,7 @@ extern void tight_loop_contents(); #define __STRING(x) #x #endif -#ifndef _MSC_VER +#if !defined(_MSC_VER) || defined(__clang__) #ifndef __noreturn #define __noreturn __attribute((noreturn)) #endif @@ -60,6 +60,12 @@ extern void tight_loop_contents(); #define __noinline __attribute__((noinline)) #endif +#ifndef __force_inline +// don't think it is critical to inline in host mode, and this is simpler than picking the +// correct attribute incantation for always_inline on different compiler versions +#define __force_inline inline +#endif + #ifndef __aligned #define __aligned(x) __attribute__((aligned(x))) #endif @@ -148,7 +154,11 @@ uint get_core_num(); static inline uint __get_current_exception(void) { return 0; + } + +void busy_wait_at_least_cycles(uint32_t minimum_cycles); + #ifdef __cplusplus } #endif diff --git a/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel b/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel index 65c9e76b2..56ed5f3c3 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel +++ b/lib/pico-sdk/rp2040/boot_stage2/BUILD.bazel @@ -76,6 +76,11 @@ cc_binary( copts = ["-fPIC"], # Incompatible with section garbage collection. features = ["-gc_sections"], + # Platforms will commonly depend on bootloader components in every + # binary via `link_extra_libs`, so we must drop these deps when + # building the bootloader binaries themselves in order to avoid a + # circular dependency. + link_extra_lib = "//bazel:empty_cc_lib", linkopts = [ "-Wl,--no-gc-sections", "-nostartfiles", diff --git a/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt b/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt index c5768785b..2798b3640 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt +++ b/lib/pico-sdk/rp2040/boot_stage2/CMakeLists.txt @@ -30,10 +30,16 @@ pico_register_common_scope_var(PICO_DEFAULT_BOOT_STAGE2_FILE) # needed by function below set(PICO_BOOT_STAGE2_DIR "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "") -add_library(boot_stage2_headers INTERFACE) +pico_add_library(boot_stage2_headers) target_include_directories(boot_stage2_headers SYSTEM INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) -# by convention the first source file name without extension is used for the binary info name +# pico_define_boot_stage2(NAME SOURCES) +# \brief\ Define a boot stage 2 target. +# +# By convention the first source file name without extension is used for the binary info name +# +# \param\ NAME The name of the boot stage 2 target +# \param\ SOURCES The source files to link into the boot stage 2 function(pico_define_boot_stage2 NAME SOURCES) add_executable(${NAME} ${SOURCES} @@ -66,15 +72,12 @@ function(pico_define_boot_stage2 NAME SOURCES) add_custom_command(OUTPUT ${ORIGINAL_BIN} DEPENDS ${NAME} COMMAND ${CMAKE_OBJCOPY} -Obinary $ ${ORIGINAL_BIN} VERBATIM) - add_custom_target(${NAME}_padded_checksummed_asm DEPENDS ${PADDED_CHECKSUMMED_ASM}) add_custom_command(OUTPUT ${PADDED_CHECKSUMMED_ASM} DEPENDS ${ORIGINAL_BIN} COMMAND ${Python3_EXECUTABLE} ${PICO_BOOT_STAGE2_DIR}/pad_checksum -s 0xffffffff ${ORIGINAL_BIN} ${PADDED_CHECKSUMMED_ASM} VERBATIM) - add_library(${NAME}_library INTERFACE) - add_dependencies(${NAME}_library ${NAME}_padded_checksummed_asm) - # not strictly (or indeed actually) a link library, but this avoids dependency cycle - target_link_libraries(${NAME}_library INTERFACE ${PADDED_CHECKSUMMED_ASM}) + add_library(${NAME}_library OBJECT ${PADDED_CHECKSUMMED_ASM}) + target_link_libraries(${NAME}_library INTERFACE "$") target_link_libraries(${NAME}_library INTERFACE boot_stage2_headers) list(GET SOURCES 0 FIRST_SOURCE) @@ -100,7 +103,12 @@ endmacro() pico_define_boot_stage2(bs2_default ${PICO_DEFAULT_BOOT_STAGE2_FILE}) +# pico_clone_default_boot_stage2(NAME) +# \brief_nodesc\ Clone the default boot stage 2 target. +# # Create a new boot stage 2 target using the default implementation for the current build (PICO_BOARD derived) +# +# \param\ NAME The name of the new boot stage 2 target function(pico_clone_default_boot_stage2 NAME) pico_define_boot_stage2(${NAME} ${PICO_DEFAULT_BOOT_STAGE2_FILE}) endfunction() diff --git a/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S b/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S index 9aa51ac57..b0e6a10fc 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S +++ b/lib/pico-sdk/rp2040/boot_stage2/boot2_w25x10cl.S @@ -144,10 +144,10 @@ regular_func _stage2_boot // status register and checking for the "RX FIFO Not Empty" flag to assert. movs r1, #SSI_SR_RFNE_BITS -00: +1: ldr r0, [r3, #SSI_SR_OFFSET] // Read status register tst r0, r1 // RFNE status flag set? - beq 00b // If not then wait + beq 1b // If not then wait // At this point CN# will be deasserted and the SPI clock will not be running. // The Winbond WX25X10CL device will be in continuous read, dual I/O mode and diff --git a/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld b/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld index f8669ab64..32978a16e 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld +++ b/lib/pico-sdk/rp2040/boot_stage2/boot_stage2.ld @@ -7,6 +7,7 @@ MEMORY { SECTIONS { . = ORIGIN(SRAM); .text : { + _start = .; /* make LLVM happy */ *(.entry) *(.text) } >SRAM diff --git a/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h b/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h index e4d32628c..568aca1ef 100644 --- a/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h +++ b/lib/pico-sdk/rp2040/boot_stage2/include/boot_stage2/config.h @@ -11,13 +11,15 @@ #include "pico.h" -// PICO_CONFIG: PICO_BUILD_BOOT_STAGE2_NAME, The name of the boot stage 2 if selected by the build, group=boot_stage2 +// PICO_CONFIG: PICO_FLASH_SPI_CLKDIV, Clock divider from clk_sys to use for serial flash communications in boot stage 2. On RP2040 this must be a multiple of 2. This define applies to compilation of the boot stage 2 not the main application, type=int, default=varies; often specified in board header, advanced=true, group=boot_stage2 + +// PICO_CONFIG: PICO_BUILD_BOOT_STAGE2_NAME, Name of the boot stage 2 if selected in the build system. This define applies to compilation of the boot stage 2 not the main application, group=boot_stage2 #ifdef PICO_BUILD_BOOT_STAGE2_NAME #define _BOOT_STAGE2_SELECTED #else // check that multiple boot stage 2 options haven't been set... -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_IS25LP080, Select boot2_is25lp080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_IS25LP080, Select boot2_is25lp080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_IS25LP080 #define PICO_BOOT_STAGE2_CHOOSE_IS25LP080 0 #elif PICO_BOOT_STAGE2_CHOOSE_IS25LP080 @@ -26,7 +28,7 @@ #endif #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25Q080, Select boot2_w25q080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25Q080, Select boot2_w25q080 as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_W25Q080 #define PICO_BOOT_STAGE2_CHOOSE_W25Q080 0 #elif PICO_BOOT_STAGE2_CHOOSE_W25Q080 @@ -35,7 +37,7 @@ #endif #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25X10CL, Select boot2_w25x10cl as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_W25X10CL, Select boot2_w25x10cl as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_W25X10CL #define PICO_BOOT_STAGE2_CHOOSE_W25X10CL 0 #elif PICO_BOOT_STAGE2_CHOOSE_W25X10CL @@ -44,7 +46,7 @@ #endif #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_AT25SF128A, Select boot2_at25sf128a as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=0, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_AT25SF128A, Select boot2_at25sf128a as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=0, group=boot_stage2 #ifndef PICO_BOOT_STAGE2_CHOOSE_AT25SF128A #define PICO_BOOT_STAGE2_CHOOSE_AT25SF128A 0 #elif PICO_BOOT_STAGE2_CHOOSE_AT25SF128A @@ -54,7 +56,7 @@ #define _BOOT_STAGE2_SELECTED #endif -// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H, Select boot2_generic_03h as the boot stage 2 when no boot stage 2 selection is made by the CMake build, type=bool, default=1, group=boot_stage2 +// PICO_CONFIG: PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H, Select boot2_generic_03h as the boot stage 2 when no boot stage 2 selection is made by the CMake build. This define applies to compilation of the boot stage 2 not the main application, type=bool, default=1, group=boot_stage2 #if defined(PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H) && PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H #ifdef _BOOT_STAGE2_SELECTED #error multiple boot stage 2 options chosen diff --git a/lib/pico-sdk/rp2040/hardware/platform_defs.h b/lib/pico-sdk/rp2040/hardware/platform_defs.h index 54d9344c8..a877710d0 100644 --- a/lib/pico-sdk/rp2040/hardware/platform_defs.h +++ b/lib/pico-sdk/rp2040/hardware/platform_defs.h @@ -46,6 +46,15 @@ #define HAS_SIO_DIVIDER 1 #define HAS_RP2040_RTC 1 + +#ifndef FPGA_CLK_SYS_HZ +#define FPGA_CLK_SYS_HZ (48 * MHZ) +#endif + +#ifndef FPGA_CLK_REF_HZ +#define FPGA_CLK_REF_HZ (12 * MHZ) +#endif + // PICO_CONFIG: XOSC_HZ, Crystal oscillator frequency in Hz, type=int, default=12000000, advanced=true, group=hardware_base // NOTE: The system and USB clocks are generated from the frequency using two PLLs. // If you override this define, or SYS_CLK_HZ/USB_CLK_HZ below, you will *also* need to add your own adjusted PLL set-up defines to @@ -61,6 +70,11 @@ #endif #endif +// PICO_CONFIG: PICO_USE_FASTEST_SUPPORTED_CLOCK, Use the fastest officially supported clock by default, type=bool, default=0, group=hardware_base +#ifndef PICO_USE_FASTEST_SUPPORTED_CLOCK +#define PICO_USE_FASTEST_SUPPORTED_CLOCK 0 +#endif + // PICO_CONFIG: SYS_CLK_HZ, System operating frequency in Hz, type=int, default=125000000, advanced=true, group=hardware_base #ifndef SYS_CLK_HZ #ifdef SYS_CLK_KHZ @@ -68,9 +82,13 @@ #elif defined(SYS_CLK_MHZ) #define SYS_CLK_HZ ((SYS_CLK_MHZ) * _u(1000000)) #else +#if PICO_USE_FASTEST_SUPPORTED_CLOCK +#define SYS_CLK_HZ _u(200000000) +#else #define SYS_CLK_HZ _u(125000000) #endif #endif +#endif // PICO_CONFIG: USB_CLK_HZ, USB clock frequency. Must be 48MHz for the USB interface to operate correctly, type=int, default=48000000, advanced=true, group=hardware_base #ifndef USB_CLK_HZ @@ -116,4 +134,6 @@ #define FIRST_USER_IRQ (NUM_IRQS - NUM_USER_IRQS) #define VTABLE_FIRST_IRQ 16 +#define REG_FIELD_WIDTH(f) (f ## _MSB + 1 - f ## _LSB) + #endif diff --git a/lib/pico-sdk/rp2040/hardware/regs/intctrl.h b/lib/pico-sdk/rp2040/hardware/regs/intctrl.h index 3190b413d..71c6eb90b 100644 --- a/lib/pico-sdk/rp2040/hardware/regs/intctrl.h +++ b/lib/pico-sdk/rp2040/hardware/regs/intctrl.h @@ -39,6 +39,12 @@ #define I2C0_IRQ 23 #define I2C1_IRQ 24 #define RTC_IRQ 25 +#define SPARE_IRQ_0 26 +#define SPARE_IRQ_1 27 +#define SPARE_IRQ_2 28 +#define SPARE_IRQ_3 29 +#define SPARE_IRQ_4 30 +#define SPARE_IRQ_5 31 #else /** * \brief Interrupt numbers on RP2040 (used as typedef \ref irq_num_t) @@ -71,6 +77,12 @@ typedef enum irq_num_rp2040 { I2C0_IRQ = 23, ///< Select I2C0's IRQ output I2C1_IRQ = 24, ///< Select I2C1's IRQ output RTC_IRQ = 25, ///< Select RTC's IRQ output + SPARE_IRQ_0 = 26, ///< Select SPARE IRQ 0 + SPARE_IRQ_1 = 27, ///< Select SPARE IRQ 1 + SPARE_IRQ_2 = 28, ///< Select SPARE IRQ 2 + SPARE_IRQ_3 = 29, ///< Select SPARE IRQ 3 + SPARE_IRQ_4 = 30, ///< Select SPARE IRQ 4 + SPARE_IRQ_5 = 31, ///< Select SPARE IRQ 5 IRQ_COUNT } irq_num_t; #endif @@ -101,6 +113,12 @@ typedef enum irq_num_rp2040 { #define isr_i2c0 isr_irq23 #define isr_i2c1 isr_irq24 #define isr_rtc isr_irq25 +#define isr_spare_0 isr_irq26 +#define isr_spare_1 isr_irq27 +#define isr_spare_2 isr_irq28 +#define isr_spare_3 isr_irq29 +#define isr_spare_4 isr_irq30 +#define isr_spare_5 isr_irq31 #endif // _INTCTRL_H diff --git a/lib/pico-sdk/rp2040/hardware/structs/busctrl.h b/lib/pico-sdk/rp2040/hardware/structs/busctrl.h index 65893227d..2302025e7 100644 --- a/lib/pico-sdk/rp2040/hardware/structs/busctrl.h +++ b/lib/pico-sdk/rp2040/hardware/structs/busctrl.h @@ -24,7 +24,6 @@ // BITMASK [BITRANGE] FIELDNAME (RESETVALUE) DESCRIPTION /** \brief Bus fabric performance counters on RP2040 (used as typedef \ref bus_ctrl_perf_counter_t) - * \ingroup hardware_busctrl */ typedef enum bus_ctrl_perf_counter_rp2040 { arbiter_rom_perf_event_access = 19, diff --git a/lib/pico-sdk/rp2350/cmsis_include/RP2350.h b/lib/pico-sdk/rp2350/cmsis_include/RP2350.h index 94d0f178c..77869ea80 100644 --- a/lib/pico-sdk/rp2350/cmsis_include/RP2350.h +++ b/lib/pico-sdk/rp2350/cmsis_include/RP2350.h @@ -4,10 +4,10 @@ * @file src/rp2_common/cmsis/stub/CMSIS/Device/RP2350/Include/RP2350.h * @brief CMSIS HeaderFile * @version 0.1 - * @date Thu Aug 8 04:04:02 2024 - * @note Generated by SVDConv V3.3.47 - * from File 'src/rp2_common/cmsis/../../rp2350/hardware_regs/RP2350.svd', - * last modified on Thu Aug 8 03:59:33 2024 + * @date Mon Jul 28 11:37:41 2025 + * @note Generated by SVDConv V3.3.45 + * from File 'src/rp2350/hardware_regs/RP2350.svd', + * last modified on Mon Jul 28 11:35:05 2025 */ @@ -2028,7 +2028,7 @@ typedef struct { /*!< POWMAN Structure ignore the power down requests. To do nothing would risk entering an unrecoverable lock-up state. Invalid requests are: any combination of power up and power down requests - any request that results in swcore boing powered and xip + any request that results in swcore being powered and xip unpowered If the request is to power down the switched-core domain then POWMAN_STATE_WAITING stays active until the processors halt. During this time the POWMAN_STATE_REQ diff --git a/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h b/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h index 30881ccc6..d85fbeb6c 100644 --- a/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h +++ b/lib/pico-sdk/rp2350/cmsis_include/system_RP2350.h @@ -1,9 +1,9 @@ /*************************************************************************//** - * @file system_RP2040.h + * @file system_RP2350.h * @brief CMSIS-Core(M) Device Peripheral Access Layer Header File for - * Device RP2040 - * @version V1.0.0 - * @date 5. May 2021 + * Device RP2350 + * @version V1.0.1 + * @date 6. Sep 2024 *****************************************************************************/ /* * Copyright (c) 2009-2021 Arm Limited. All rights reserved. @@ -26,8 +26,8 @@ * SPDX-License-Identifier: BSD-3-Clause */ -#ifndef _CMSIS_SYSTEM_RP2040_H -#define _CMSIS_SYSTEM_RP2040_H +#ifndef _CMSIS_SYSTEM_RP2350_H +#define _CMSIS_SYSTEM_RP2350_H #ifdef __cplusplus extern "C" { @@ -62,4 +62,4 @@ extern void SystemCoreClockUpdate (void); } #endif -#endif /* _CMSIS_SYSTEM_RP2040_H */ +#endif /* _CMSIS_SYSTEM_RP2350_H */ diff --git a/lib/pico-sdk/rp2350/hardware/regs/clocks.h b/lib/pico-sdk/rp2350/hardware/regs/clocks.h index fd560c910..ce46345f8 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/clocks.h +++ b/lib/pico-sdk/rp2350/hardware/regs/clocks.h @@ -615,7 +615,7 @@ // Description : Clock control, can be changed on-the-fly (except for auxsrc) #define CLOCKS_CLK_SYS_CTRL_OFFSET _u(0x0000003c) #define CLOCKS_CLK_SYS_CTRL_BITS _u(0x000000e1) -#define CLOCKS_CLK_SYS_CTRL_RESET _u(0x00000000) +#define CLOCKS_CLK_SYS_CTRL_RESET _u(0x00000041) // ----------------------------------------------------------------------------- // Field : CLOCKS_CLK_SYS_CTRL_AUXSRC // Description : Selects the auxiliary clock source, will glitch when switching @@ -625,7 +625,7 @@ // 0x3 -> xosc_clksrc // 0x4 -> clksrc_gpin0 // 0x5 -> clksrc_gpin1 -#define CLOCKS_CLK_SYS_CTRL_AUXSRC_RESET _u(0x0) +#define CLOCKS_CLK_SYS_CTRL_AUXSRC_RESET _u(0x2) #define CLOCKS_CLK_SYS_CTRL_AUXSRC_BITS _u(0x000000e0) #define CLOCKS_CLK_SYS_CTRL_AUXSRC_MSB _u(7) #define CLOCKS_CLK_SYS_CTRL_AUXSRC_LSB _u(5) @@ -642,7 +642,7 @@ // fly // 0x0 -> clk_ref // 0x1 -> clksrc_clk_sys_aux -#define CLOCKS_CLK_SYS_CTRL_SRC_RESET _u(0x0) +#define CLOCKS_CLK_SYS_CTRL_SRC_RESET _u(0x1) #define CLOCKS_CLK_SYS_CTRL_SRC_BITS _u(0x00000001) #define CLOCKS_CLK_SYS_CTRL_SRC_MSB _u(0) #define CLOCKS_CLK_SYS_CTRL_SRC_LSB _u(0) diff --git a/lib/pico-sdk/rp2350/hardware/regs/dreq.h b/lib/pico-sdk/rp2350/hardware/regs/dreq.h index 6d126c0df..edcdae60b 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/dreq.h +++ b/lib/pico-sdk/rp2350/hardware/regs/dreq.h @@ -121,8 +121,8 @@ typedef enum dreq_num_rp2350 { DREQ_PWM_WRAP7 = 39, ///< Select PWM Counter 7's Wrap Value as DREQ DREQ_PWM_WRAP8 = 40, ///< Select PWM Counter 8's Wrap Value as DREQ DREQ_PWM_WRAP9 = 41, ///< Select PWM Counter 9's Wrap Value as DREQ - DREQ_PWM_WRAP10 = 42, ///< Select PWM Counter 0's Wrap Value as DREQ - DREQ_PWM_WRAP11 = 43, ///< Select PWM Counter 1's Wrap Value as DREQ + DREQ_PWM_WRAP10 = 42, ///< Select PWM Counter 10's Wrap Value as DREQ + DREQ_PWM_WRAP11 = 43, ///< Select PWM Counter 11's Wrap Value as DREQ DREQ_I2C0_TX = 44, ///< Select I2C0's TX FIFO as DREQ DREQ_I2C0_RX = 45, ///< Select I2C0's RX FIFO as DREQ DREQ_I2C1_TX = 46, ///< Select I2C1's TX FIFO as DREQ @@ -135,8 +135,8 @@ typedef enum dreq_num_rp2350 { DREQ_CORESIGHT = 53, ///< Select CORESIGHT as DREQ DREQ_SHA256 = 54, ///< Select SHA256 as DREQ DREQ_DMA_TIMER0 = 59, ///< Select DMA_TIMER0 as DREQ - DREQ_DMA_TIMER1 = 60, ///< Select DMA_TIMER0 as DREQ - DREQ_DMA_TIMER2 = 61, ///< Select DMA_TIMER1 as DREQ + DREQ_DMA_TIMER1 = 60, ///< Select DMA_TIMER1 as DREQ + DREQ_DMA_TIMER2 = 61, ///< Select DMA_TIMER2 as DREQ DREQ_DMA_TIMER3 = 62, ///< Select DMA_TIMER3 as DREQ DREQ_FORCE = 63, ///< Select FORCE as DREQ DREQ_COUNT diff --git a/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h b/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h index efdf434b3..6e108e2b7 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h +++ b/lib/pico-sdk/rp2350/hardware/regs/glitch_detector.h @@ -37,8 +37,7 @@ #define GLITCH_DETECTOR_ARM_VALUE_YES _u(0x0000) // ============================================================================= // Register : GLITCH_DETECTOR_DISARM -// Description : None -// Forcibly disarm the glitch detectors, if they are armed by OTP. +// Description : Forcibly disarm the glitch detectors, if they are armed by OTP. // Ignored if ARM is YES. // // This register is Secure read/write only. @@ -142,8 +141,7 @@ #define GLITCH_DETECTOR_SENSITIVITY_DET0_ACCESS "RW" // ============================================================================= // Register : GLITCH_DETECTOR_LOCK -// Description : None -// Write any nonzero value to disable writes to ARM, DISARM, +// Description : Write any nonzero value to disable writes to ARM, DISARM, // SENSITIVITY and LOCK. This register is Secure read/write only. #define GLITCH_DETECTOR_LOCK_OFFSET _u(0x0000000c) #define GLITCH_DETECTOR_LOCK_BITS _u(0x000000ff) diff --git a/lib/pico-sdk/rp2350/hardware/regs/intctrl.h b/lib/pico-sdk/rp2350/hardware/regs/intctrl.h index 96ce815e4..1e19e33d1 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/intctrl.h +++ b/lib/pico-sdk/rp2350/hardware/regs/intctrl.h @@ -59,12 +59,12 @@ #define PLL_USB_IRQ 43 #define POWMAN_IRQ_POW 44 #define POWMAN_IRQ_TIMER 45 -#define SPAREIRQ_IRQ_0 46 -#define SPAREIRQ_IRQ_1 47 -#define SPAREIRQ_IRQ_2 48 -#define SPAREIRQ_IRQ_3 49 -#define SPAREIRQ_IRQ_4 50 -#define SPAREIRQ_IRQ_5 51 +#define SPARE_IRQ_0 46 +#define SPARE_IRQ_1 47 +#define SPARE_IRQ_2 48 +#define SPARE_IRQ_3 49 +#define SPARE_IRQ_4 50 +#define SPARE_IRQ_5 51 #else /** * \brief Interrupt numbers on RP2350 (used as typedef \ref irq_num_t) @@ -79,8 +79,8 @@ typedef enum irq_num_rp2350 { TIMER1_IRQ_1 = 5, ///< Select TIMER1's IRQ 1 output TIMER1_IRQ_2 = 6, ///< Select TIMER1's IRQ 2 output TIMER1_IRQ_3 = 7, ///< Select TIMER1's IRQ 3 output - PWM_IRQ_WRAP_0 = 8, ///< Select PWM's IRQ_WRAP 0 output - PWM_IRQ_WRAP_1 = 9, ///< Select PWM's IRQ_WRAP 1 output + PWM_IRQ_WRAP_0 = 8, ///< Select PWM's WRAP_0 IRQ output + PWM_IRQ_WRAP_1 = 9, ///< Select PWM's WRAP_1 IRQ output DMA_IRQ_0 = 10, ///< Select DMA's IRQ 0 output DMA_IRQ_1 = 11, ///< Select DMA's IRQ 1 output DMA_IRQ_2 = 12, ///< Select DMA's IRQ 2 output @@ -96,27 +96,27 @@ typedef enum irq_num_rp2350 { IO_IRQ_BANK0_NS = 22, ///< Select IO_BANK0_NS's IRQ output IO_IRQ_QSPI = 23, ///< Select IO_QSPI's IRQ output IO_IRQ_QSPI_NS = 24, ///< Select IO_QSPI_NS's IRQ output - SIO_IRQ_FIFO = 25, ///< Select SIO's IRQ_FIFO output - SIO_IRQ_BELL = 26, ///< Select SIO's IRQ_BELL output - SIO_IRQ_FIFO_NS = 27, ///< Select SIO_NS's IRQ_FIFO output - SIO_IRQ_BELL_NS = 28, ///< Select SIO_NS's IRQ_BELL output - SIO_IRQ_MTIMECMP = 29, ///< Select SIO_IRQ_MTIMECMP's IRQ output + SIO_IRQ_FIFO = 25, ///< Select SIO's FIFO IRQ output + SIO_IRQ_BELL = 26, ///< Select SIO's BELL IRQ output + SIO_IRQ_FIFO_NS = 27, ///< Select SIO_NS's FIFO IRQ output + SIO_IRQ_BELL_NS = 28, ///< Select SIO_NS's BELL IRQ output + SIO_IRQ_MTIMECMP = 29, ///< Select SIO's MTIMECMP IRQ output CLOCKS_IRQ = 30, ///< Select CLOCKS's IRQ output SPI0_IRQ = 31, ///< Select SPI0's IRQ output SPI1_IRQ = 32, ///< Select SPI1's IRQ output UART0_IRQ = 33, ///< Select UART0's IRQ output UART1_IRQ = 34, ///< Select UART1's IRQ output - ADC_IRQ_FIFO = 35, ///< Select ADC's IRQ_FIFO output + ADC_IRQ_FIFO = 35, ///< Select ADC's FIFO IRQ output I2C0_IRQ = 36, ///< Select I2C0's IRQ output I2C1_IRQ = 37, ///< Select I2C1's IRQ output OTP_IRQ = 38, ///< Select OTP's IRQ output TRNG_IRQ = 39, ///< Select TRNG's IRQ output - PROC0_IRQ_CTI = 40, ///< Select PROC0's IRQ_CTI output - PROC1_IRQ_CTI = 41, ///< Select PROC1's IRQ_CTI output + PROC0_IRQ_CTI = 40, ///< Select PROC0's CTI IRQ output + PROC1_IRQ_CTI = 41, ///< Select PROC1's CTI IRQ output PLL_SYS_IRQ = 42, ///< Select PLL_SYS's IRQ output PLL_USB_IRQ = 43, ///< Select PLL_USB's IRQ output - POWMAN_IRQ_POW = 44, ///< Select POWMAN's IRQ_POW output - POWMAN_IRQ_TIMER = 45, ///< Select POWMAN's IRQ_TIMER output + POWMAN_IRQ_POW = 44, ///< Select POWMAN's POW IRQ output + POWMAN_IRQ_TIMER = 45, ///< Select POWMAN's TIMER IRQ output SPARE_IRQ_0 = 46, ///< Select SPARE IRQ 0 SPARE_IRQ_1 = 47, ///< Select SPARE IRQ 1 SPARE_IRQ_2 = 48, ///< Select SPARE IRQ 2 diff --git a/lib/pico-sdk/rp2350/hardware/regs/pio.h b/lib/pico-sdk/rp2350/hardware/regs/pio.h index 4a18b5c6f..d20569708 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/pio.h +++ b/lib/pico-sdk/rp2350/hardware/regs/pio.h @@ -461,8 +461,7 @@ // ============================================================================= // Register : PIO_DBG_PADOUT // Description : Read to sample the pad output values PIO is currently driving -// to the GPIOs. On RP2040 there are 30 GPIOs, so the two most -// significant bits are hardwired to 0. +// to the GPIOs. #define PIO_DBG_PADOUT_OFFSET _u(0x0000003c) #define PIO_DBG_PADOUT_BITS _u(0xffffffff) #define PIO_DBG_PADOUT_RESET _u(0x00000000) @@ -472,8 +471,7 @@ // ============================================================================= // Register : PIO_DBG_PADOE // Description : Read to sample the pad output enables (direction) PIO is -// currently driving to the GPIOs. On RP2040 there are 30 GPIOs, -// so the two most significant bits are hardwired to 0. +// currently driving to the GPIOs. #define PIO_DBG_PADOE_OFFSET _u(0x00000040) #define PIO_DBG_PADOE_BITS _u(0xffffffff) #define PIO_DBG_PADOE_RESET _u(0x00000000) diff --git a/lib/pico-sdk/rp2350/hardware/regs/powman.h b/lib/pico-sdk/rp2350/hardware/regs/powman.h index edfbabbcc..8beb5650b 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/powman.h +++ b/lib/pico-sdk/rp2350/hardware/regs/powman.h @@ -944,7 +944,7 @@ // requests and ignore the power down requests. To do nothing // would risk entering an unrecoverable lock-up state. Invalid // requests are: any combination of power up and power down -// requests any request that results in swcore boing powered and +// requests any request that results in swcore being powered and // xip unpowered If the request is to power down the switched-core // domain then POWMAN_STATE_WAITING stays active until the // processors halt. During this time the POWMAN_STATE_REQ field @@ -957,6 +957,7 @@ #define POWMAN_STATE_RESET _u(0x0000000f) // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_CHANGING +// Description : Indicates a power state change is in progress #define POWMAN_STATE_CHANGING_RESET _u(0x0) #define POWMAN_STATE_CHANGING_BITS _u(0x00002000) #define POWMAN_STATE_CHANGING_MSB _u(13) @@ -964,6 +965,9 @@ #define POWMAN_STATE_CHANGING_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_WAITING +// Description : Indicates the power manager has received a state change request +// and is waiting for other actions to complete before executing +// it #define POWMAN_STATE_WAITING_RESET _u(0x0) #define POWMAN_STATE_WAITING_BITS _u(0x00001000) #define POWMAN_STATE_WAITING_MSB _u(12) @@ -971,8 +975,8 @@ #define POWMAN_STATE_WAITING_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_BAD_HW_REQ -// Description : Bad hardware initiated state request. Went back to state 0 -// (i.e. everything powered up) +// Description : Invalid hardware initiated state request, power up requests +// actioned, power down requests ignored #define POWMAN_STATE_BAD_HW_REQ_RESET _u(0x0) #define POWMAN_STATE_BAD_HW_REQ_BITS _u(0x00000800) #define POWMAN_STATE_BAD_HW_REQ_MSB _u(11) @@ -980,7 +984,7 @@ #define POWMAN_STATE_BAD_HW_REQ_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_BAD_SW_REQ -// Description : Bad software initiated state request. No action taken. +// Description : Invalid software initiated state request ignored #define POWMAN_STATE_BAD_SW_REQ_RESET _u(0x0) #define POWMAN_STATE_BAD_SW_REQ_BITS _u(0x00000400) #define POWMAN_STATE_BAD_SW_REQ_MSB _u(10) @@ -988,9 +992,8 @@ #define POWMAN_STATE_BAD_SW_REQ_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_PWRUP_WHILE_WAITING -// Description : Request ignored because of a pending pwrup request. See -// current_pwrup_req. Note this blocks powering up AND powering -// down. +// Description : Indicates that a power state change request was ignored because +// of a pending power state change request #define POWMAN_STATE_PWRUP_WHILE_WAITING_RESET _u(0x0) #define POWMAN_STATE_PWRUP_WHILE_WAITING_BITS _u(0x00000200) #define POWMAN_STATE_PWRUP_WHILE_WAITING_MSB _u(9) @@ -998,6 +1001,8 @@ #define POWMAN_STATE_PWRUP_WHILE_WAITING_ACCESS "WC" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_REQ_IGNORED +// Description : Indicates that a software state change request was ignored +// because it clashed with an ongoing hardware or debugger request #define POWMAN_STATE_REQ_IGNORED_RESET _u(0x0) #define POWMAN_STATE_REQ_IGNORED_BITS _u(0x00000100) #define POWMAN_STATE_REQ_IGNORED_MSB _u(8) @@ -1005,6 +1010,8 @@ #define POWMAN_STATE_REQ_IGNORED_ACCESS "WC" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_REQ +// Description : This is written by software or hardware to request a new power +// state #define POWMAN_STATE_REQ_RESET _u(0x0) #define POWMAN_STATE_REQ_BITS _u(0x000000f0) #define POWMAN_STATE_REQ_MSB _u(7) @@ -1012,6 +1019,7 @@ #define POWMAN_STATE_REQ_ACCESS "RW" // ----------------------------------------------------------------------------- // Field : POWMAN_STATE_CURRENT +// Description : Indicates the current power state #define POWMAN_STATE_CURRENT_RESET _u(0xf) #define POWMAN_STATE_CURRENT_BITS _u(0x0000000f) #define POWMAN_STATE_CURRENT_MSB _u(3) @@ -1019,8 +1027,7 @@ #define POWMAN_STATE_CURRENT_ACCESS "RO" // ============================================================================= // Register : POWMAN_POW_FASTDIV -// Description : None -// divides the POWMAN clock to provide a tick for the delay module +// Description : divides the POWMAN clock to provide a tick for the delay module // and state machines // when clk_pow is running from the slow clock it is not divided // when clk_pow is running from the fast clock it is divided by @@ -1187,6 +1194,10 @@ #define POWMAN_EXT_TIME_REF_SOURCE_SEL_MSB _u(1) #define POWMAN_EXT_TIME_REF_SOURCE_SEL_LSB _u(0) #define POWMAN_EXT_TIME_REF_SOURCE_SEL_ACCESS "RW" +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO12 _u(0x0) +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO20 _u(0x1) +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO14 _u(0x2) +#define POWMAN_EXT_TIME_REF_SOURCE_SEL_VALUE_GPIO22 _u(0x3) // ============================================================================= // Register : POWMAN_LPOSC_FREQ_KHZ_INT // Description : Informs the AON Timer of the integer component of the clock @@ -1241,8 +1252,7 @@ #define POWMAN_XOSC_FREQ_KHZ_FRAC_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_63TO48 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_63TO48_OFFSET _u(0x00000060) @@ -1253,8 +1263,7 @@ #define POWMAN_SET_TIME_63TO48_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_47TO32 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_47TO32_OFFSET _u(0x00000064) @@ -1265,8 +1274,7 @@ #define POWMAN_SET_TIME_47TO32_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_31TO16 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_31TO16_OFFSET _u(0x00000068) @@ -1277,8 +1285,7 @@ #define POWMAN_SET_TIME_31TO16_ACCESS "RW" // ============================================================================= // Register : POWMAN_SET_TIME_15TO0 -// Description : None -// For setting the time, do not use for reading the time, use +// Description : For setting the time, do not use for reading the time, use // POWMAN_READ_TIME_UPPER and POWMAN_READ_TIME_LOWER. This field // must only be written when POWMAN_TIMER_RUN=0 #define POWMAN_SET_TIME_15TO0_OFFSET _u(0x0000006c) @@ -1289,8 +1296,7 @@ #define POWMAN_SET_TIME_15TO0_ACCESS "RW" // ============================================================================= // Register : POWMAN_READ_TIME_UPPER -// Description : None -// For reading bits 63:32 of the timer. When reading all 64 bits +// Description : For reading bits 63:32 of the timer. When reading all 64 bits // it is possible for the LOWER count to rollover during the read. // It is recommended to read UPPER, then LOWER, then re-read UPPER // and, if it has changed, re-read LOWER. @@ -1302,8 +1308,7 @@ #define POWMAN_READ_TIME_UPPER_ACCESS "RO" // ============================================================================= // Register : POWMAN_READ_TIME_LOWER -// Description : None -// For reading bits 31:0 of the timer. +// Description : For reading bits 31:0 of the timer. #define POWMAN_READ_TIME_LOWER_OFFSET _u(0x00000074) #define POWMAN_READ_TIME_LOWER_BITS _u(0xffffffff) #define POWMAN_READ_TIME_LOWER_RESET _u(0x00000000) @@ -1312,8 +1317,7 @@ #define POWMAN_READ_TIME_LOWER_ACCESS "RO" // ============================================================================= // Register : POWMAN_ALARM_TIME_63TO48 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_63TO48_OFFSET _u(0x00000078) #define POWMAN_ALARM_TIME_63TO48_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_63TO48_RESET _u(0x00000000) @@ -1322,8 +1326,7 @@ #define POWMAN_ALARM_TIME_63TO48_ACCESS "RW" // ============================================================================= // Register : POWMAN_ALARM_TIME_47TO32 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_47TO32_OFFSET _u(0x0000007c) #define POWMAN_ALARM_TIME_47TO32_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_47TO32_RESET _u(0x00000000) @@ -1332,8 +1335,7 @@ #define POWMAN_ALARM_TIME_47TO32_ACCESS "RW" // ============================================================================= // Register : POWMAN_ALARM_TIME_31TO16 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_31TO16_OFFSET _u(0x00000080) #define POWMAN_ALARM_TIME_31TO16_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_31TO16_RESET _u(0x00000000) @@ -1342,8 +1344,7 @@ #define POWMAN_ALARM_TIME_31TO16_ACCESS "RW" // ============================================================================= // Register : POWMAN_ALARM_TIME_15TO0 -// Description : None -// This field must only be written when POWMAN_ALARM_ENAB=0 +// Description : This field must only be written when POWMAN_ALARM_ENAB=0 #define POWMAN_ALARM_TIME_15TO0_OFFSET _u(0x00000084) #define POWMAN_ALARM_TIME_15TO0_BITS _u(0x0000ffff) #define POWMAN_ALARM_TIME_15TO0_RESET _u(0x00000000) diff --git a/lib/pico-sdk/rp2350/hardware/regs/rosc.h b/lib/pico-sdk/rp2350/hardware/regs/rosc.h index 4865c2ee3..4caa07a7b 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/rosc.h +++ b/lib/pico-sdk/rp2350/hardware/regs/rosc.h @@ -39,9 +39,9 @@ // Field : ROSC_CTRL_FREQ_RANGE // Description : Controls the number of delay stages in the ROSC ring // LOW uses stages 0 to 7 -// MEDIUM uses stages 2 to 7 -// HIGH uses stages 4 to 7 -// TOOHIGH uses stages 6 to 7 and should not be used because its +// MEDIUM uses stages 0 to 5 +// HIGH uses stages 0 to 3 +// TOOHIGH uses stages 0 to 1 and should not be used because its // frequency exceeds design specifications // The clock output will not glitch when changing the range up one // step at a time @@ -77,7 +77,7 @@ // DS1_RANDOM=1 #define ROSC_FREQA_OFFSET _u(0x00000004) #define ROSC_FREQA_BITS _u(0xffff77ff) -#define ROSC_FREQA_RESET _u(0x00000000) +#define ROSC_FREQA_RESET _u(0x00000088) // ----------------------------------------------------------------------------- // Field : ROSC_FREQA_PASSWD // Description : Set to 0x9696 to apply the settings @@ -108,7 +108,7 @@ // ----------------------------------------------------------------------------- // Field : ROSC_FREQA_DS1_RANDOM // Description : Randomises the stage 1 drive strength -#define ROSC_FREQA_DS1_RANDOM_RESET _u(0x0) +#define ROSC_FREQA_DS1_RANDOM_RESET _u(0x1) #define ROSC_FREQA_DS1_RANDOM_BITS _u(0x00000080) #define ROSC_FREQA_DS1_RANDOM_MSB _u(7) #define ROSC_FREQA_DS1_RANDOM_LSB _u(7) @@ -124,7 +124,7 @@ // ----------------------------------------------------------------------------- // Field : ROSC_FREQA_DS0_RANDOM // Description : Randomises the stage 0 drive strength -#define ROSC_FREQA_DS0_RANDOM_RESET _u(0x0) +#define ROSC_FREQA_DS0_RANDOM_RESET _u(0x1) #define ROSC_FREQA_DS0_RANDOM_BITS _u(0x00000008) #define ROSC_FREQA_DS0_RANDOM_MSB _u(3) #define ROSC_FREQA_DS0_RANDOM_LSB _u(3) @@ -206,7 +206,7 @@ // On power-up this field is initialised to WAKE // An invalid write will also select WAKE // Warning: setup the irq before selecting dormant mode -// 0x636f6d61 -> dormant +// 0x636f6d61 -> DORMANT // 0x77616b65 -> WAKE #define ROSC_DORMANT_OFFSET _u(0x00000010) #define ROSC_DORMANT_BITS _u(0xffffffff) diff --git a/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h b/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h index f5ff378ab..a375a19d1 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h +++ b/lib/pico-sdk/rp2350/hardware/regs/rvcsr.h @@ -86,7 +86,7 @@ // In addition the following custom extensions are configured: // Xh3bm, Xh3power, Xh3irq, Xh3pmpm #define RVCSR_MISA_OFFSET _u(0x00000301) -#define RVCSR_MISA_BITS _u(0xc0901107) +#define RVCSR_MISA_BITS _u(0xc0b511bf) #define RVCSR_MISA_RESET _u(0x40901105) // ----------------------------------------------------------------------------- // Field : RVCSR_MISA_MXL @@ -106,6 +106,14 @@ #define RVCSR_MISA_X_LSB _u(23) #define RVCSR_MISA_X_ACCESS "RO" // ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_V +// Description : Vector extension (not implemented). +#define RVCSR_MISA_V_RESET _u(0x0) +#define RVCSR_MISA_V_BITS _u(0x00200000) +#define RVCSR_MISA_V_MSB _u(21) +#define RVCSR_MISA_V_LSB _u(21) +#define RVCSR_MISA_V_ACCESS "RO" +// ----------------------------------------------------------------------------- // Field : RVCSR_MISA_U // Description : Value of 1 indicates U-mode is implemented. #define RVCSR_MISA_U_RESET _u(0x1) @@ -114,6 +122,22 @@ #define RVCSR_MISA_U_LSB _u(20) #define RVCSR_MISA_U_ACCESS "RO" // ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_S +// Description : Supervisor extension (not implemented). +#define RVCSR_MISA_S_RESET _u(0x0) +#define RVCSR_MISA_S_BITS _u(0x00040000) +#define RVCSR_MISA_S_MSB _u(18) +#define RVCSR_MISA_S_LSB _u(18) +#define RVCSR_MISA_S_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_Q +// Description : Quad-precision floating point extension (not implemented). +#define RVCSR_MISA_Q_RESET _u(0x0) +#define RVCSR_MISA_Q_BITS _u(0x00010000) +#define RVCSR_MISA_Q_MSB _u(16) +#define RVCSR_MISA_Q_LSB _u(16) +#define RVCSR_MISA_Q_ACCESS "RO" +// ----------------------------------------------------------------------------- // Field : RVCSR_MISA_M // Description : Value of 1 indicates the M extension (integer multiply/divide) // is implemented. @@ -132,6 +156,39 @@ #define RVCSR_MISA_I_LSB _u(8) #define RVCSR_MISA_I_ACCESS "RO" // ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_H +// Description : Hypervisor extension (not implemented, I agree it would be +// pretty cool on a microcontroller through). +#define RVCSR_MISA_H_RESET _u(0x0) +#define RVCSR_MISA_H_BITS _u(0x00000080) +#define RVCSR_MISA_H_MSB _u(7) +#define RVCSR_MISA_H_LSB _u(7) +#define RVCSR_MISA_H_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_F +// Description : Single-precision floating point extension (not implemented). +#define RVCSR_MISA_F_RESET _u(0x0) +#define RVCSR_MISA_F_BITS _u(0x00000020) +#define RVCSR_MISA_F_MSB _u(5) +#define RVCSR_MISA_F_LSB _u(5) +#define RVCSR_MISA_F_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_E +// Description : RV32E/64E base ISA (not implemented). +#define RVCSR_MISA_E_RESET _u(0x0) +#define RVCSR_MISA_E_BITS _u(0x00000010) +#define RVCSR_MISA_E_MSB _u(4) +#define RVCSR_MISA_E_LSB _u(4) +#define RVCSR_MISA_E_ACCESS "RO" +// ----------------------------------------------------------------------------- +// Field : RVCSR_MISA_D +// Description : Double-precision floating point extension (not implemented). +#define RVCSR_MISA_D_RESET _u(0x0) +#define RVCSR_MISA_D_BITS _u(0x00000008) +#define RVCSR_MISA_D_MSB _u(3) +#define RVCSR_MISA_D_LSB _u(3) +#define RVCSR_MISA_D_ACCESS "RO" +// ----------------------------------------------------------------------------- // Field : RVCSR_MISA_C // Description : Value of 1 indicates the C extension (compressed instructions) // is implemented. @@ -207,7 +264,7 @@ // Description : Timer interrupt enable. The processor transfers to the timer // interrupt vector when `mie.mtie`, `mip.mtip` and `mstatus.mie` // are all 1, unless a software or external interrupt request is -// also valid at this time. +// also both pending and enabled at this time. #define RVCSR_MIE_MTIE_RESET _u(0x0) #define RVCSR_MIE_MTIE_BITS _u(0x00000080) #define RVCSR_MIE_MTIE_MSB _u(7) @@ -216,9 +273,9 @@ // ----------------------------------------------------------------------------- // Field : RVCSR_MIE_MSIE // Description : Software interrupt enable. The processor transfers to the -// software interrupt vector `mie.msie`, `mip.msip` and +// software interrupt vector when `mie.msie`, `mip.msip` and // `mstatus.mie` are all 1, unless an external interrupt request -// is also valid at this time. +// is also both pending and enabled at this time. #define RVCSR_MIE_MSIE_RESET _u(0x0) #define RVCSR_MIE_MSIE_BITS _u(0x00000008) #define RVCSR_MIE_MSIE_MSB _u(3) @@ -336,7 +393,7 @@ #define RVCSR_MENVCFGH_RESET _u(0x00000000) #define RVCSR_MENVCFGH_MSB _u(31) #define RVCSR_MENVCFGH_LSB _u(0) -#define RVCSR_MENVCFGH_ACCESS "RW" +#define RVCSR_MENVCFGH_ACCESS "-" // ============================================================================= // Register : RVCSR_MCOUNTINHIBIT // Description : Count inhibit register for `mcycle`/`minstret` @@ -732,7 +789,7 @@ // Description : Timer interrupt pending. The processor transfers to the timer // interrupt vector when `mie.mtie`, `mip.mtip` and `mstatus.mie` // are all 1, unless a software or external interrupt request is -// also valid at this time. +// also both pending and enabled at this time. #define RVCSR_MIP_MTIP_RESET _u(0x0) #define RVCSR_MIP_MTIP_BITS _u(0x00000080) #define RVCSR_MIP_MTIP_MSB _u(7) @@ -741,9 +798,9 @@ // ----------------------------------------------------------------------------- // Field : RVCSR_MIP_MSIP // Description : Software interrupt pending. The processor transfers to the -// software interrupt vector `mie.msie`, `mip.msip` and +// software interrupt vector when `mie.msie`, `mip.msip` and // `mstatus.mie` are all 1, unless an external interrupt request -// is also valid at this time. +// is also both pending and enabled at this time. #define RVCSR_MIP_MSIP_RESET _u(0x0) #define RVCSR_MIP_MSIP_BITS _u(0x00000008) #define RVCSR_MIP_MSIP_MSB _u(3) @@ -3099,14 +3156,18 @@ #define RVCSR_MVENDORID_RESET _u(0x00000000) // ----------------------------------------------------------------------------- // Field : RVCSR_MVENDORID_BANK -#define RVCSR_MVENDORID_BANK_RESET "-" +// Description : Value of 9 indicates 9 continuation codes, which is JEP106 bank +// 10. +#define RVCSR_MVENDORID_BANK_RESET _u(0x0000009) #define RVCSR_MVENDORID_BANK_BITS _u(0xffffff80) #define RVCSR_MVENDORID_BANK_MSB _u(31) #define RVCSR_MVENDORID_BANK_LSB _u(7) #define RVCSR_MVENDORID_BANK_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : RVCSR_MVENDORID_OFFSET -#define RVCSR_MVENDORID_OFFSET_RESET "-" +// Description : ID 0x13 in bank 10 is the JEP106 ID for Raspberry Pi Ltd, the +// vendor of RP2350. +#define RVCSR_MVENDORID_OFFSET_RESET _u(0x13) #define RVCSR_MVENDORID_OFFSET_BITS _u(0x0000007f) #define RVCSR_MVENDORID_OFFSET_MSB _u(6) #define RVCSR_MVENDORID_OFFSET_LSB _u(0) @@ -3122,10 +3183,11 @@ #define RVCSR_MARCHID_ACCESS "RO" // ============================================================================= // Register : RVCSR_MIMPID -// Description : Implementation ID +// Description : Implementation ID. On RP2350 this reads as 0x86fc4e3f, which is +// release v1.0-rc1 of Hazard3. #define RVCSR_MIMPID_OFFSET _u(0x00000f13) #define RVCSR_MIMPID_BITS _u(0xffffffff) -#define RVCSR_MIMPID_RESET "-" +#define RVCSR_MIMPID_RESET _u(0x86fc4e3f) #define RVCSR_MIMPID_MSB _u(31) #define RVCSR_MIMPID_LSB _u(0) #define RVCSR_MIMPID_ACCESS "RO" diff --git a/lib/pico-sdk/rp2350/hardware/regs/syscfg.h b/lib/pico-sdk/rp2350/hardware/regs/syscfg.h index 455ebf175..74fb596f4 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/syscfg.h +++ b/lib/pico-sdk/rp2350/hardware/regs/syscfg.h @@ -255,7 +255,10 @@ // ============================================================================= // Register : SYSCFG_AUXCTRL // Description : Auxiliary system control register -// * Bits 7:2: Reserved +// * Bits 7:3: Reserved +// +// * Bit 2: Set to mask OTP power analogue power supply detection +// from resetting OTP controller and PSM // // * Bit 1: When clear, the LPOSC output is XORed into the TRNG // ROSC output as an additional, uncorrelated entropy source. When diff --git a/lib/pico-sdk/rp2350/hardware/regs/ticks.h b/lib/pico-sdk/rp2350/hardware/regs/ticks.h index 79e13523d..3dac57216 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/ticks.h +++ b/lib/pico-sdk/rp2350/hardware/regs/ticks.h @@ -36,8 +36,7 @@ #define TICKS_PROC0_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC0_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_PROC0_CYCLES_OFFSET _u(0x00000004) #define TICKS_PROC0_CYCLES_BITS _u(0x000001ff) #define TICKS_PROC0_CYCLES_RESET _u(0x00000000) @@ -46,8 +45,7 @@ #define TICKS_PROC0_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC0_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_PROC0_COUNT_OFFSET _u(0x00000008) #define TICKS_PROC0_COUNT_BITS _u(0x000001ff) @@ -79,8 +77,7 @@ #define TICKS_PROC1_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC1_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_PROC1_CYCLES_OFFSET _u(0x00000010) #define TICKS_PROC1_CYCLES_BITS _u(0x000001ff) #define TICKS_PROC1_CYCLES_RESET _u(0x00000000) @@ -89,8 +86,7 @@ #define TICKS_PROC1_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_PROC1_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_PROC1_COUNT_OFFSET _u(0x00000014) #define TICKS_PROC1_COUNT_BITS _u(0x000001ff) @@ -122,8 +118,7 @@ #define TICKS_TIMER0_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER0_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_TIMER0_CYCLES_OFFSET _u(0x0000001c) #define TICKS_TIMER0_CYCLES_BITS _u(0x000001ff) #define TICKS_TIMER0_CYCLES_RESET _u(0x00000000) @@ -132,8 +127,7 @@ #define TICKS_TIMER0_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER0_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_TIMER0_COUNT_OFFSET _u(0x00000020) #define TICKS_TIMER0_COUNT_BITS _u(0x000001ff) @@ -165,8 +159,7 @@ #define TICKS_TIMER1_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER1_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_TIMER1_CYCLES_OFFSET _u(0x00000028) #define TICKS_TIMER1_CYCLES_BITS _u(0x000001ff) #define TICKS_TIMER1_CYCLES_RESET _u(0x00000000) @@ -175,8 +168,7 @@ #define TICKS_TIMER1_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_TIMER1_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_TIMER1_COUNT_OFFSET _u(0x0000002c) #define TICKS_TIMER1_COUNT_BITS _u(0x000001ff) @@ -208,8 +200,7 @@ #define TICKS_WATCHDOG_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_WATCHDOG_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_WATCHDOG_CYCLES_OFFSET _u(0x00000034) #define TICKS_WATCHDOG_CYCLES_BITS _u(0x000001ff) #define TICKS_WATCHDOG_CYCLES_RESET _u(0x00000000) @@ -218,8 +209,7 @@ #define TICKS_WATCHDOG_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_WATCHDOG_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_WATCHDOG_COUNT_OFFSET _u(0x00000038) #define TICKS_WATCHDOG_COUNT_BITS _u(0x000001ff) @@ -251,8 +241,7 @@ #define TICKS_RISCV_CTRL_ENABLE_ACCESS "RW" // ============================================================================= // Register : TICKS_RISCV_CYCLES -// Description : None -// Total number of clk_tick cycles before the next tick. +// Description : Total number of clk_tick cycles before the next tick. #define TICKS_RISCV_CYCLES_OFFSET _u(0x00000040) #define TICKS_RISCV_CYCLES_BITS _u(0x000001ff) #define TICKS_RISCV_CYCLES_RESET _u(0x00000000) @@ -261,8 +250,7 @@ #define TICKS_RISCV_CYCLES_ACCESS "RW" // ============================================================================= // Register : TICKS_RISCV_COUNT -// Description : None -// Count down timer: the remaining number clk_tick cycles before +// Description : Count down timer: the remaining number clk_tick cycles before // the next tick is generated. #define TICKS_RISCV_COUNT_OFFSET _u(0x00000044) #define TICKS_RISCV_COUNT_BITS _u(0x000001ff) diff --git a/lib/pico-sdk/rp2350/hardware/regs/usb.h b/lib/pico-sdk/rp2350/hardware/regs/usb.h index fbf1b7b36..4bb142bf5 100644 --- a/lib/pico-sdk/rp2350/hardware/regs/usb.h +++ b/lib/pico-sdk/rp2350/hardware/regs/usb.h @@ -1082,13 +1082,14 @@ #define USB_SIE_STATUS_SPEED_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : USB_SIE_STATUS_SUSPENDED -// Description : Bus in suspended state. Valid for device. Device will go into -// suspend if neither Keep Alive / SOF frames are enabled. +// Description : Bus in suspended state. Valid for device and host. Host and +// device will go into suspend if neither Keep Alive / SOF frames +// are enabled. #define USB_SIE_STATUS_SUSPENDED_RESET _u(0x0) #define USB_SIE_STATUS_SUSPENDED_BITS _u(0x00000010) #define USB_SIE_STATUS_SUSPENDED_MSB _u(4) #define USB_SIE_STATUS_SUSPENDED_LSB _u(4) -#define USB_SIE_STATUS_SUSPENDED_ACCESS "WC" +#define USB_SIE_STATUS_SUSPENDED_ACCESS "RO" // ----------------------------------------------------------------------------- // Field : USB_SIE_STATUS_LINE_STATE // Description : USB bus line state diff --git a/lib/pico-sdk/rp2350/hardware/structs/busctrl.h b/lib/pico-sdk/rp2350/hardware/structs/busctrl.h index 2eb83a992..b38797b25 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/busctrl.h +++ b/lib/pico-sdk/rp2350/hardware/structs/busctrl.h @@ -24,7 +24,6 @@ // BITMASK [BITRANGE] FIELDNAME (RESETVALUE) DESCRIPTION /** \brief Bus fabric performance counters on RP2350 (used as typedef \ref bus_ctrl_perf_counter_t) - * \ingroup hardware_busctrl */ typedef enum bus_ctrl_perf_counter_rp2350 { arbiter_rom_perf_event_access = 19, diff --git a/lib/pico-sdk/rp2350/hardware/structs/powman.h b/lib/pico-sdk/rp2350/hardware/structs/powman.h index a81890e3c..3aa8015c0 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/powman.h +++ b/lib/pico-sdk/rp2350/hardware/structs/powman.h @@ -137,14 +137,14 @@ typedef struct { _REG_(POWMAN_STATE_OFFSET) // POWMAN_STATE // This register controls the power state of the 4 power domains - // 0x00002000 [13] CHANGING (0) - // 0x00001000 [12] WAITING (0) - // 0x00000800 [11] BAD_HW_REQ (0) Bad hardware initiated state request - // 0x00000400 [10] BAD_SW_REQ (0) Bad software initiated state request - // 0x00000200 [9] PWRUP_WHILE_WAITING (0) Request ignored because of a pending pwrup request - // 0x00000100 [8] REQ_IGNORED (0) - // 0x000000f0 [7:4] REQ (0x0) - // 0x0000000f [3:0] CURRENT (0xf) + // 0x00002000 [13] CHANGING (0) Indicates a power state change is in progress + // 0x00001000 [12] WAITING (0) Indicates the power manager has received a state change... + // 0x00000800 [11] BAD_HW_REQ (0) Invalid hardware initiated state request, power up... + // 0x00000400 [10] BAD_SW_REQ (0) Invalid software initiated state request ignored + // 0x00000200 [9] PWRUP_WHILE_WAITING (0) Indicates that a power state change request was ignored... + // 0x00000100 [8] REQ_IGNORED (0) Indicates that a software state change request was... + // 0x000000f0 [7:4] REQ (0x0) This is written by software or hardware to request a new... + // 0x0000000f [3:0] CURRENT (0xf) Indicates the current power state io_rw_32 state; _REG_(POWMAN_POW_FASTDIV_OFFSET) // POWMAN_POW_FASTDIV diff --git a/lib/pico-sdk/rp2350/hardware/structs/rosc.h b/lib/pico-sdk/rp2350/hardware/structs/rosc.h index 73503cc15..4147cfb41 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/rosc.h +++ b/lib/pico-sdk/rp2350/hardware/structs/rosc.h @@ -35,9 +35,9 @@ typedef struct { // 0xffff0000 [31:16] PASSWD (0x0000) Set to 0x9696 to apply the settings + // 0x00007000 [14:12] DS3 (0x0) Stage 3 drive strength // 0x00000700 [10:8] DS2 (0x0) Stage 2 drive strength - // 0x00000080 [7] DS1_RANDOM (0) Randomises the stage 1 drive strength + // 0x00000080 [7] DS1_RANDOM (1) Randomises the stage 1 drive strength // 0x00000070 [6:4] DS1 (0x0) Stage 1 drive strength - // 0x00000008 [3] DS0_RANDOM (0) Randomises the stage 0 drive strength + // 0x00000008 [3] DS0_RANDOM (1) Randomises the stage 0 drive strength // 0x00000007 [2:0] DS0 (0x0) Stage 0 drive strength io_rw_32 freqa; diff --git a/lib/pico-sdk/rp2350/hardware/structs/syscfg.h b/lib/pico-sdk/rp2350/hardware/structs/syscfg.h index 8909c0dbf..71660e5fb 100644 --- a/lib/pico-sdk/rp2350/hardware/structs/syscfg.h +++ b/lib/pico-sdk/rp2350/hardware/structs/syscfg.h @@ -72,7 +72,7 @@ typedef struct { _REG_(SYSCFG_AUXCTRL_OFFSET) // SYSCFG_AUXCTRL // Auxiliary system control register - // 0x000000ff [7:0] AUXCTRL (0x00) * Bits 7:2: Reserved + // 0x000000ff [7:0] AUXCTRL (0x00) * Bits 7:3: Reserved io_rw_32 auxctrl; } syscfg_hw_t; diff --git a/src/rp2040/rp2350_bootrom.c b/src/rp2040/rp2350_bootrom.c index 257fb3d44..e691ac7e8 100644 --- a/src/rp2040/rp2350_bootrom.c +++ b/src/rp2040/rp2350_bootrom.c @@ -5,10 +5,10 @@ // This file may be distributed under the terms of the GNU GPLv3 license. #include // uint32_t -#include "boot/picoboot_constants.h" // REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL #include "hardware/address_mapped.h" // static_assert +#include "boot/bootrom_constants.h" // RT_FLAG_FUNC_ARM_NONSEC +#include "boot/picoboot_constants.h" // REBOOT2_FLAG_REBOOT_TYPE_BOOTSEL #include "internal.h" // bootrom_read_unique_id -#include "pico/bootrom_constants.h" // RT_FLAG_FUNC_ARM_NONSEC static void * rom_func_lookup(uint32_t code) From 3f72e519ed08ff8b647d4e18f4cd78b38a33d91e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 12 Dec 2025 23:03:01 -0500 Subject: [PATCH 011/108] atsamd: Fix possible buffer overflow in usbserial.c The USB buffer size register PCKSIZE.SIZE was not being assigned correctly. As a result, it was possible for an incoming USB transmission to write past the allocated buffer space, which could corrupt memory of other storage. In particular, in some cases gcc may layout ram in such a way that the trailing bytes of an incoming message might overlap the buffer for an outgoing message. This could cause sporadic transmit errors and unstable connections. Fix by correctly configuring the PCKSIZE.SIZE register. Signed-off-by: Kevin O'Connor --- src/atsamd/usbserial.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/atsamd/usbserial.c b/src/atsamd/usbserial.c index 460de4645..d083beb86 100644 --- a/src/atsamd/usbserial.c +++ b/src/atsamd/usbserial.c @@ -25,27 +25,30 @@ static uint8_t __aligned(4) acmin[USB_CDC_EP_ACM_SIZE]; static uint8_t __aligned(4) bulkout[USB_CDC_EP_BULK_OUT_SIZE]; static uint8_t __aligned(4) bulkin[USB_CDC_EP_BULK_IN_SIZE]; +// Convert 64, 32, 16, 8 sized buffer to 3, 2, 1, 0 for PCKSIZE.SIZE register +#define BSIZE(bufname) (__builtin_ctz(sizeof(bufname)) - 3) + static UsbDeviceDescriptor usb_desc[] = { [0] = { { { .ADDR.reg = (uint32_t)ep0out, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(ep0out) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(ep0out)), }, { .ADDR.reg = (uint32_t)ep0in, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(ep0in) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(ep0in)), }, } }, [USB_CDC_EP_ACM] = { { { }, { .ADDR.reg = (uint32_t)acmin, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(acmin) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(acmin)), }, } }, [USB_CDC_EP_BULK_OUT] = { { { .ADDR.reg = (uint32_t)bulkout, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(bulkout) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(bulkout)), }, { }, } }, @@ -53,7 +56,7 @@ static UsbDeviceDescriptor usb_desc[] = { { }, { .ADDR.reg = (uint32_t)bulkin, - .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(sizeof(bulkin) >> 4), + .PCKSIZE.reg = USB_DEVICE_PCKSIZE_SIZE(BSIZE(bulkin)), }, } }, }; From a8cbc935522554b6676a787ca39883bc16d32441 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 17 Dec 2025 10:48:04 -0500 Subject: [PATCH 012/108] bus: Verify that software i2c pins are all on the same mcu Signed-off-by: Kevin O'Connor --- klippy/extras/bus.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/klippy/extras/bus.py b/klippy/extras/bus.py index b04fbe764..19e278418 100644 --- a/klippy/extras/bus.py +++ b/klippy/extras/bus.py @@ -240,6 +240,10 @@ def MCU_I2C_from_config(config, default_addr=None, default_speed=100000): for name in ['scl', 'sda']] sw_pin_params = [ppins.lookup_pin(config.get(name), share_type=name) for name in sw_pin_names] + for pin_params in sw_pin_params: + if pin_params['chip'] != i2c_mcu: + raise ppins.error("%s: i2c pins must be on same mcu" % ( + config.get_name(),)) sw_pins = tuple([pin_params['pin'] for pin_params in sw_pin_params]) bus = None else: From 3a3e9fa2f14e6d0aa7403283d1600d1122c74a87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Dec 2025 21:10:51 -0500 Subject: [PATCH 013/108] rp2040: Support rp2350b extra gpios Add support for gpio31 through gpio47 on rp2350, as these pins are available on the rp2350b chips. Signed-off-by: Kevin O'Connor --- src/rp2040/gpio.c | 62 ++++++++++++++++++++++++++++++----------------- src/rp2040/gpio.h | 2 ++ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/rp2040/gpio.c b/src/rp2040/gpio.c index 98e077897..a564d1282 100644 --- a/src/rp2040/gpio.c +++ b/src/rp2040/gpio.c @@ -1,6 +1,6 @@ // GPIO functions on rp2040 // -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -19,12 +19,16 @@ * Pin mappings ****************************************************************/ -DECL_ENUMERATION_RANGE("pin", "gpio0", 0, 30); +#define NUM_GPIO (CONFIG_MACH_RP2040 ? 30 : 48) + +DECL_ENUMERATION_RANGE("pin", "gpio0", 0, NUM_GPIO); // Set the mode and extended function of a pin void gpio_peripheral(uint32_t gpio, int func, int pull_up) { + if (gpio >= NUM_GPIO) + shutdown("Not a valid pin"); padsbank0_hw->io[gpio] = ( PADS_BANK0_GPIO0_IE_BITS | (PADS_BANK0_GPIO0_DRIVE_VALUE_4MA << PADS_BANK0_GPIO0_DRIVE_MSB) @@ -35,9 +39,12 @@ gpio_peripheral(uint32_t gpio, int func, int pull_up) // Convert a register and bit location back to an integer pin identifier static int -mask_to_pin(uint32_t mask) +mask_to_pin(void *sio, uint32_t mask) { - return ffs(mask)-1; + int pin = ffs(mask)-1; + if (CONFIG_MACH_RP2350 && sio != (void*)sio_hw) + pin += 32; + return pin; } @@ -48,22 +55,26 @@ mask_to_pin(uint32_t mask) struct gpio_out gpio_out_setup(uint8_t pin, uint8_t val) { - if (pin >= 30) - goto fail; - struct gpio_out g = { .bit=1<= NUM_GPIO) + shutdown("Not a valid pin"); + void *sio = (void*)sio_hw; + if (CONFIG_MACH_RP2350 && pin >= 32) { + pin -= 32; + sio += 4; + } + struct gpio_out g = { .sio=sio, .bit=1<gpio_oe_set = g.bit; + sio_hw_t *sio = g.sio; + sio->gpio_oe_set = g.bit; gpio_peripheral(pin, 5, 0); irq_restore(flag); } @@ -71,7 +82,8 @@ gpio_out_reset(struct gpio_out g, uint8_t val) void gpio_out_toggle_noirq(struct gpio_out g) { - sio_hw->gpio_togl = g.bit; + sio_hw_t *sio = g.sio; + sio->gpio_togl = g.bit; } void @@ -83,37 +95,43 @@ gpio_out_toggle(struct gpio_out g) void gpio_out_write(struct gpio_out g, uint8_t val) { + sio_hw_t *sio = g.sio; if (val) - sio_hw->gpio_set = g.bit; + sio->gpio_set = g.bit; else - sio_hw->gpio_clr = g.bit; + sio->gpio_clr = g.bit; } struct gpio_in gpio_in_setup(uint8_t pin, int8_t pull_up) { - if (pin >= 30) - goto fail; - struct gpio_in g = { .bit=1<= NUM_GPIO) + shutdown("Not a valid pin"); + void *sio = (void*)sio_hw; + if (CONFIG_MACH_RP2350 && pin >= 32) { + pin -= 32; + sio += 4; + } + struct gpio_in g = { .sio=sio, .bit=1<gpio_oe_clr = g.bit; + sio_hw_t *sio = g.sio; + sio->gpio_oe_clr = g.bit; irq_restore(flag); } uint8_t gpio_in_read(struct gpio_in g) { - return !!(sio_hw->gpio_in & g.bit); + sio_hw_t *sio = g.sio; + return !!(sio->gpio_in & g.bit); } diff --git a/src/rp2040/gpio.h b/src/rp2040/gpio.h index 0dd393bfe..23d432f63 100644 --- a/src/rp2040/gpio.h +++ b/src/rp2040/gpio.h @@ -4,6 +4,7 @@ #include // uint32_t struct gpio_out { + void *sio; uint32_t bit; }; struct gpio_out gpio_out_setup(uint8_t pin, uint8_t val); @@ -13,6 +14,7 @@ void gpio_out_toggle(struct gpio_out g); void gpio_out_write(struct gpio_out g, uint8_t val); struct gpio_in { + void *sio; uint32_t bit; }; struct gpio_in gpio_in_setup(uint8_t pin, int8_t pull_up); From a3e24d2172525aaada595d86114735b28e7a822f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 15 Dec 2025 21:52:47 -0500 Subject: [PATCH 014/108] rp2040: Add support for ADC on rp2350b chips Signed-off-by: Kevin O'Connor --- src/rp2040/adc.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/rp2040/adc.c b/src/rp2040/adc.c index 2daf380a6..477858c54 100644 --- a/src/rp2040/adc.c +++ b/src/rp2040/adc.c @@ -1,6 +1,6 @@ // ADC functions on rp2040 // -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -10,6 +10,7 @@ #include "hardware/structs/adc.h" // adc_hw #include "hardware/structs/padsbank0.h" // padsbank0_hw #include "hardware/structs/resets.h" // RESETS_RESET_ADC_BITS +#include "hardware/structs/sysinfo.h" // sysinfo_hw #include "internal.h" // enable_pclock #include "sched.h" // sched_shutdown @@ -21,7 +22,19 @@ DECL_ENUMERATION("pin", "ADC_TEMPERATURE", ADC_TEMPERATURE_PIN); struct gpio_adc gpio_adc_setup(uint32_t pin) { - if ((pin < 26 || pin > 29) && pin != ADC_TEMPERATURE_PIN) + uint32_t min_gpio = 26, max_gpio = 29, adc_temp_chan = 4; +#if CONFIG_MACH_RP2350 + // Check for rp2350b package + if (!is_enabled_pclock(RESETS_RESET_SYSINFO_BITS)) + enable_pclock(RESETS_RESET_SYSINFO_BITS); + if (!(sysinfo_hw->package_sel & SYSINFO_PACKAGE_SEL_BITS)) { + min_gpio = 40; + max_gpio = 47; + adc_temp_chan = 8; + } +#endif + + if ((pin < min_gpio || pin > max_gpio) && pin != ADC_TEMPERATURE_PIN) shutdown("Not a valid ADC pin"); // Enable the ADC @@ -32,10 +45,10 @@ gpio_adc_setup(uint32_t pin) uint8_t chan; if (pin == ADC_TEMPERATURE_PIN) { - chan = 4; + chan = adc_temp_chan; adc_hw->cs |= ADC_CS_TS_EN_BITS; } else { - chan = pin - 26; + chan = pin - min_gpio; padsbank0_hw->io[pin] = PADS_BANK0_GPIO0_OD_BITS; } From 59a754c48c478fc35c638c0cb3ad018ca4b58164 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 6 Feb 2025 13:43:15 -0500 Subject: [PATCH 015/108] stm32: Implement manual double buffering tx for usbotg It is possible for USB host controllers to send back-to-back IN tokens which only gives the MCU ~3us to queue the next USB packet in the hardware. That can be difficult to do if the MCU has to wake up the task code. The stm32 "usbotg" hardware does not support a builtin generic double buffering transmit capability, but it is possible to load the next packet directly from the irq handler code. This change adds support for queuing the next packet destined for the host so that the USB irq handler can directly load it into the hardware. Signed-off-by: Kevin O'Connor --- src/stm32/Kconfig | 3 +++ src/stm32/usbotg.c | 52 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 1e0df93d2..72be8a4d1 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -156,6 +156,9 @@ config HAVE_STM32_USBFS config HAVE_STM32_USBOTG bool default y if MACH_STM32F2 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32H7 +config STM32_USB_DOUBLE_BUFFER_TX + bool + default y config HAVE_STM32_CANBUS bool default y if MACH_STM32F1 || MACH_STM32F2 || MACH_STM32F4x5 || MACH_STM32F446 || MACH_STM32F0x2 diff --git a/src/stm32/usbotg.c b/src/stm32/usbotg.c index b2d52456d..b41daf48c 100644 --- a/src/stm32/usbotg.c +++ b/src/stm32/usbotg.c @@ -1,6 +1,6 @@ // Hardware interface to "USB OTG (on the go) controller" on stm32 // -// Copyright (C) 2019 Kevin O'Connor +// Copyright (C) 2019-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -119,6 +119,24 @@ fifo_write_packet(uint32_t ep, const uint8_t *src, uint32_t len) return len; } +// Write a packet to a tx fifo (optimized for already aligned data) +static int +fifo_write_packet_fast(uint32_t ep, const uint32_t *src, uint32_t len) +{ + void *fifo = EPFIFO(ep); + USB_OTG_INEndpointTypeDef *epi = EPIN(ep); + uint32_t ctl = epi->DIEPCTL; + if (ctl & USB_OTG_DIEPCTL_EPENA) + return -1; + epi->DIEPINT = USB_OTG_DIEPINT_XFRC; + epi->DIEPTSIZ = len | (1 << USB_OTG_DIEPTSIZ_PKTCNT_Pos); + epi->DIEPCTL = ctl | USB_OTG_DIEPCTL_EPENA | USB_OTG_DIEPCTL_CNAK; + uint32_t i; + for (i=0; i < DIV_ROUND_UP(len, sizeof(uint32_t)); i++) + writel(fifo, src[i]); + return 0; +} + // Read a packet from the rx queue static int_fast8_t fifo_read_packet(uint8_t *dest, uint_fast8_t max_len) @@ -208,6 +226,12 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) return ret; } +// Storage for "bulk in" transmissions for a kind of manual "double buffering" +static struct { + uint32_t len; + uint32_t buf[USB_CDC_EP_BULK_IN_SIZE / sizeof(uint32_t)]; +} TX_BUF; + int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) { @@ -219,10 +243,21 @@ usb_send_bulk_in(void *data, uint_fast8_t len) return len; } if (ctl & USB_OTG_DIEPCTL_EPENA) { - // Wait for space to transmit + if (!CONFIG_STM32_USB_DOUBLE_BUFFER_TX || TX_BUF.len || !len) { + // Wait for space to transmit + OTGD->DAINTMSK |= 1 << USB_CDC_EP_BULK_IN; + usb_irq_enable(); + return -1; + } + // Buffer next packet for transmission from irq handler + len = len > USB_CDC_EP_BULK_IN_SIZE ? USB_CDC_EP_BULK_IN_SIZE : len; + uint32_t blocks = DIV_ROUND_UP(len, sizeof(uint32_t)); + TX_BUF.buf[blocks-1] = 0; + memcpy(TX_BUF.buf, data, len); + TX_BUF.len = len; OTGD->DAINTMSK |= 1 << USB_CDC_EP_BULK_IN; usb_irq_enable(); - return -1; + return len; } int_fast8_t ret = fifo_write_packet(USB_CDC_EP_BULK_IN, data, len); usb_irq_enable(); @@ -373,6 +408,8 @@ usb_set_configure(void) | USB_OTG_GRSTCTL_TXFFLSH); while (OTG->GRSTCTL & USB_OTG_GRSTCTL_TXFFLSH) ; + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) + TX_BUF.len = 0; usb_irq_enable(); } @@ -401,8 +438,15 @@ OTG_FS_IRQHandler(void) OTGD->DAINTMSK = msk & ~daint; if (pend & (1 << 0)) usb_notify_ep0(); - if (pend & (1 << USB_CDC_EP_BULK_IN)) + if (pend & (1 << USB_CDC_EP_BULK_IN)) { usb_notify_bulk_in(); + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX && TX_BUF.len) { + int ret = fifo_write_packet_fast(USB_CDC_EP_BULK_IN + , TX_BUF.buf, TX_BUF.len); + if (!ret) + TX_BUF.len = 0; + } + } } } From c3b660dfbe61f8851fba9541f6658484e389d5eb Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 12:30:32 -0500 Subject: [PATCH 016/108] stm32: Simplify USBx_IRQn per-chip definitions in usbfs.c Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 5385c956c..2e1bc420d 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -1,6 +1,6 @@ // Hardware interface to "fullspeed USB controller" // -// Copyright (C) 2018-2023 Kevin O'Connor +// Copyright (C) 2018-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -19,29 +19,27 @@ // Transfer memory is accessed with 32bits, but contains only 16bits of data typedef volatile uint32_t epmword_t; #define WSIZE 2 - #define USBx_IRQn USB_LP_IRQn -#elif CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32L4 +#elif CONFIG_MACH_STM32F0 || CONFIG_MACH_STM32L4 || CONFIG_MACH_STM32G4 // Transfer memory is accessed with 16bits and contains 16bits of data typedef volatile uint16_t epmword_t; #define WSIZE 2 - #define USBx_IRQn USB_IRQn -#elif CONFIG_MACH_STM32G4 - // Transfer memory is accessed with 16bits and contains 16bits of data - typedef volatile uint16_t epmword_t; - #define WSIZE 2 - #define USBx_IRQn USB_LP_IRQn #elif CONFIG_MACH_STM32G0 // Transfer memory is accessed with 32bits and contains 32bits of data typedef volatile uint32_t epmword_t; #define WSIZE 4 +#endif + +// Different chips have different names for the USB irq +#if CONFIG_MACH_STM32F1 || CONFIG_MACH_STM32G4 + #define USBx_IRQn USB_LP_IRQn +#elif CONFIG_MACH_STM32G0B1 + #define USBx_IRQn USB_UCPD1_2_IRQn +#else #define USBx_IRQn USB_IRQn #endif // The stm32g0 has slightly different register names #if CONFIG_MACH_STM32G0 - #if CONFIG_MACH_STM32G0B1 - #define USB_IRQn USB_UCPD1_2_IRQn - #endif #define USB USB_DRD_FS #define USB_PMAADDR USB_DRD_PMAADDR #define USB_EPADDR_FIELD USB_CHEP_ADDR @@ -55,8 +53,8 @@ // Some chip variants do not define these fields #ifndef USB_EP_DTOG_TX_Pos -#define USB_EP_DTOG_TX_Pos 6 -#define USB_EP_DTOG_RX_Pos 14 + #define USB_EP_DTOG_TX_Pos 6 + #define USB_EP_DTOG_RX_Pos 14 #endif From dc622f4ac329de2d987cbdfd24477641bdb742fb Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 14 Dec 2025 13:32:38 -0500 Subject: [PATCH 017/108] stm32: Allow disabling double buffering transmit in usbfs.c Only enable double buffering transmit if CONFIG_STM32_USB_DOUBLE_BUFFER_TX is set. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 2e1bc420d..3751a4f5a 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -242,8 +242,9 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) static uint32_t bulk_in_push_pos, bulk_in_pop_flag; #define BI_START 2 -int_fast8_t -usb_send_bulk_in(void *data, uint_fast8_t len) +// Send bulk packet to host with double buffering optimization +static int_fast8_t +usb_send_bulk_in_double_buffer(void *data, uint_fast8_t len) { if (readl(&bulk_in_pop_flag)) // No buffer space available @@ -275,6 +276,21 @@ usb_send_bulk_in(void *data, uint_fast8_t len) return len; } +// Send bulk usb packet to host +int_fast8_t +usb_send_bulk_in(void *data, uint_fast8_t len) +{ + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) + return usb_send_bulk_in_double_buffer(data, len); + uint32_t ep = USB_CDC_EP_BULK_IN, epr = USB_EPR[ep]; + if ((epr & USB_EPTX_STAT) != USB_EP_TX_NAK) + // No buffer space available + return -1; + btable_write_packet(ep, BUFTX, data, len); + USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT, USB_EP_TX_VALID); + return len; +} + int_fast8_t usb_read_ep0(void *data, uint_fast8_t max_len) { @@ -333,9 +349,10 @@ usb_set_configure(void) bulk_out_pop_count = 0; USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPRX_STAT, USB_EP_RX_VALID); - ep = USB_CDC_EP_BULK_IN; - bulk_in_push_pos = BI_START; - writel(&bulk_in_pop_flag, 0); + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) { + bulk_in_push_pos = BI_START; + writel(&bulk_in_pop_flag, 0); + } } @@ -360,9 +377,12 @@ usb_reset(void) bulk_out_push_flag = USB_EP_DTOG_TX; ep = USB_CDC_EP_BULK_IN; - USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK | USB_EP_KIND - | USB_EP_TX_NAK); - bulk_in_pop_flag = USB_EP_DTOG_RX; + uint32_t bi_epr_flags = USB_CDC_EP_BULK_IN | USB_EP_BULK | USB_EP_TX_NAK; + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) { + bi_epr_flags |= USB_EP_KIND; + bulk_in_pop_flag = USB_EP_DTOG_RX; + } + USB_EPR[ep] = bi_epr_flags; USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM; USB->DADDR = USB_DADDR_EF; @@ -382,9 +402,12 @@ USB_IRQHandler(void) bulk_out_push_flag = 0; usb_notify_bulk_out(); } else if (ep == USB_CDC_EP_BULK_IN) { - USB_EPR[ep] = (calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0) - | bulk_in_pop_flag); - bulk_in_pop_flag = 0; + uint32_t ne = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); + if (CONFIG_STM32_USB_DOUBLE_BUFFER_TX) { + ne |= bulk_in_pop_flag; + bulk_in_pop_flag = 0; + } + USB_EPR[ep] = ne; usb_notify_bulk_in(); } else if (ep == 0) { USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); From a40beb7b1b340257cce1c21a6e45442603a77cbc Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 15 Nov 2025 22:14:36 +0100 Subject: [PATCH 018/108] probe_eddy_current: filter noisy calibration points A misplaced sensor or a misconfigured one can return unreliable results. Assist with this by refusing to use the too noisy points. Filter noisy points by the frequency difference to noise ratio. Signed-off-by: Timofey Titovets --- klippy/extras/probe_eddy_current.py | 87 +++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 96c766708..4dc46e413 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -134,16 +134,68 @@ class EddyCalibration: raise self.printer.command_error( "Failed calibration - incomplete sensor data") return cal + + def _median(self, values): + values = sorted(values) + n = len(values) + if n % 2 == 0: + return (values[n//2 - 1] + values[n//2]) / 2.0 + return values[n // 2] def calc_freqs(self, meas): - total_count = total_variance = 0 positions = {} for pos, freqs in meas.items(): count = len(freqs) freq_avg = float(sum(freqs)) / count - positions[pos] = freq_avg - total_count += count - total_variance += sum([(f - freq_avg)**2 for f in freqs]) - return positions, math.sqrt(total_variance / total_count), total_count + mads = [abs(f - freq_avg) for f in freqs] + mad = self._median(mads) + positions[pos] = (freq_avg, mad, count) + return positions + def validate_calibration_data(self, positions): + last_freq = 40000000. + last_pos = last_mad = .0 + gcode = self.printer.lookup_object("gcode") + filtered = [] + mad_hz_total = .0 + mad_mm_total = .0 + samples_count = 0 + for pos, (freq_avg, mad_hz, count) in sorted(positions.items()): + if freq_avg > last_freq: + gcode.respond_info( + "Frequency stops decreasing at step %.3f" % (pos)) + break + diff_mad = math.sqrt(last_mad**2 + mad_hz**2) + # Calculate if samples have a significant difference + freq_diff = last_freq - freq_avg + last_freq = freq_avg + if freq_diff < 2.5 * diff_mad: + gcode.respond_info( + "Frequency too noisy at step %.3f -> %.3f" % ( + last_pos, pos)) + gcode.respond_info( + "Frequency diff: %.3f, MAD_Hz: %.3f -> MAD_Hz: %.3f" % ( + freq_diff, last_mad, mad_hz + )) + break + last_mad = mad_hz + delta_dist = pos - last_pos + last_pos = pos + # MAD is Median Absolute Deviation to Frequency avg ~ delta_hz_1 + # Signal is delta_hz_2 / delta_dist + # SNR ~= delta_hz_1 / (delta_hz_2 / delta_mm) = d_1 * d_mm / d_2 + mad_mm = mad_hz * delta_dist / freq_diff + filtered.append((pos, freq_avg, mad_hz, mad_mm)) + mad_hz_total += mad_hz + mad_mm_total += mad_mm + samples_count += count + avg_mad = mad_hz_total / len(filtered) + avg_mad_mm = mad_mm_total / len(filtered) + gcode.respond_info( + "probe_eddy_current: noise %.6fmm, MAD_Hz=%.3f in %d queries\n" % ( + avg_mad_mm, avg_mad, samples_count)) + freq_list = [freq for _, freq, _, _ in filtered] + freq_diff = max(freq_list) - min(freq_list) + gcode.respond_info("Total frequency range: %.3f Hz\n" % (freq_diff)) + return filtered def post_manual_probe(self, kin_pos): if kin_pos is None: # Manual Probe was aborted @@ -166,24 +218,27 @@ class EddyCalibration: # Perform calibration movement and capture cal = self.do_calibration_moves(self.probe_speed) # Calculate each sample position average and variance - positions, std, total = self.calc_freqs(cal) - last_freq = 0. - for pos, freq in reversed(sorted(positions.items())): - if freq <= last_freq: - raise self.printer.command_error( - "Failed calibration - frequency not increasing each step") - last_freq = freq + _positions = self.calc_freqs(cal) + # Fix Z position offset + positions = {} + for k in _positions: + v = _positions[k] + k = k - probe_calibrate_z + positions[k] = v + filtered = self.validate_calibration_data(positions) + if len(filtered) <= 8: + raise self.printer.command_error( + "Failed calibration - No usable data") gcode = self.printer.lookup_object("gcode") gcode.respond_info( - "probe_eddy_current: stddev=%.3f in %d queries\n" "The SAVE_CONFIG command will update the printer config file\n" - "and restart the printer." % (std, total)) + "and restart the printer.") # Save results cal_contents = [] - for i, (pos, freq) in enumerate(sorted(positions.items())): + for i, (pos, freq, _, _) in enumerate(filtered): if not i % 3: cal_contents.append('\n') - cal_contents.append("%.6f:%.3f" % (pos - probe_calibrate_z, freq)) + cal_contents.append("%.6f:%.3f" % (pos, freq)) cal_contents.append(',') cal_contents.pop() configfile = self.printer.lookup_object('configfile') From 8b58aa130294994571b365b83c6364f930a03244 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Mon, 24 Nov 2025 21:46:58 +0100 Subject: [PATCH 019/108] probe_eddy_current: show noise at distinct calibration points Signed-off-by: Timofey Titovets --- klippy/extras/probe_eddy_current.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 4dc46e413..779a904fb 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -195,6 +195,13 @@ class EddyCalibration: freq_list = [freq for _, freq, _, _ in filtered] freq_diff = max(freq_list) - min(freq_list) gcode.respond_info("Total frequency range: %.3f Hz\n" % (freq_diff)) + points = [0.25, 0.5, 1.0, 2.0, 3.0] + for pos, _, mad_hz, mad_mm in filtered: + if len(points) and points[0] <= pos: + points.pop(0) + msg = "z_offset: %.3f # noise %.6fmm, MAD_Hz=%.3f\n" % ( + pos, mad_mm, mad_hz) + gcode.respond_info(msg) return filtered def post_manual_probe(self, kin_pos): if kin_pos is None: From 94cbde75174d2f4beb92dce45963f118feab060d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 15:24:39 -0500 Subject: [PATCH 020/108] _klipper3d: Rename build-translations.sh to build-website.sh Rename the build script to make it more clear that it builds the entire github hosted website. Signed-off-by: Kevin O'Connor --- .github/workflows/klipper3d-deploy.yaml | 2 +- .../_klipper3d/{build-translations.sh => build-website.sh} | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) rename docs/_klipper3d/{build-translations.sh => build-website.sh} (93%) diff --git a/.github/workflows/klipper3d-deploy.yaml b/.github/workflows/klipper3d-deploy.yaml index 609644bbc..076be8dac 100644 --- a/.github/workflows/klipper3d-deploy.yaml +++ b/.github/workflows/klipper3d-deploy.yaml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: pip install -r docs/_klipper3d/mkdocs-requirements.txt - name: Build MkDocs Pages - run: docs/_klipper3d/build-translations.sh + run: docs/_klipper3d/build-website.sh - name: Deploy uses: JamesIves/github-pages-deploy-action@v4.4.3 with: diff --git a/docs/_klipper3d/build-translations.sh b/docs/_klipper3d/build-website.sh similarity index 93% rename from docs/_klipper3d/build-translations.sh rename to docs/_klipper3d/build-website.sh index 4a0117c24..e8fd62504 100755 --- a/docs/_klipper3d/build-translations.sh +++ b/docs/_klipper3d/build-website.sh @@ -1,7 +1,8 @@ #!/bin/bash -# This script extracts the Klipper translations and builds multiple -# mdocs sites - one for each supported language. See the README file -# for additional details. +# This script creates the main klipper3d.org website hosted on github. +# It extracts the Klipper translations and builds multiple mdocs sites +# - one for each supported language. See the README file for +# additional details. MKDOCS_DIR="docs/_klipper3d/" WORK_DIR="work/" From d9f5da7196fde06b5c60550050df829a34af7a54 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 15:27:16 -0500 Subject: [PATCH 021/108] _klipper3d: Remove unnecessary files copied into the main website Signed-off-by: Kevin O'Connor --- docs/_klipper3d/build-website.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/_klipper3d/build-website.sh b/docs/_klipper3d/build-website.sh index e8fd62504..83240949c 100755 --- a/docs/_klipper3d/build-website.sh +++ b/docs/_klipper3d/build-website.sh @@ -23,6 +23,10 @@ done < <(egrep -v '^ *(#|$)' ${TRANS_FILE}) echo "building site for en" mkdocs build -f ${MKDOCS_MAIN} +# Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) +rm -r ${PWD}/site/_klipper3d +rm ${PWD}/site/sitemap.xml ${PWD}/site/sitemap.xml.gz + # Build each additional language website while IFS="," read dirname langsite langdesc langsearch; do new_docs_dir="${WORK_DIR}lang/${langsite}/docs/" @@ -82,4 +86,9 @@ while IFS="," read dirname langsite langdesc langsearch; do mkdir -p "${PWD}/site/${langsite}/" ln -sf "${PWD}/site/${langsite}/" "${WORK_DIR}lang/${langsite}/site" mkdocs build -f "${new_mkdocs_file}" + + # Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) + rm -r "${PWD}/site/${langsite}/_klipper3d" + rm "${PWD}/site/${langsite}/sitemap.xml" "${PWD}/site/${langsite}/sitemap.xml.gz" + done < <(egrep -v '^ *(#|$)' ${TRANS_FILE}) From 5d24122c0439a2be020622dea161cab54ebe24be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 18:25:59 -0500 Subject: [PATCH 022/108] _klipper3d: Make sure custom css file is preserved Signed-off-by: Kevin O'Connor --- docs/_klipper3d/build-website.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/_klipper3d/build-website.sh b/docs/_klipper3d/build-website.sh index 83240949c..9c1ae0205 100755 --- a/docs/_klipper3d/build-website.sh +++ b/docs/_klipper3d/build-website.sh @@ -24,7 +24,8 @@ echo "building site for en" mkdocs build -f ${MKDOCS_MAIN} # Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) -rm -r ${PWD}/site/_klipper3d +rm -rf ${PWD}/site/_klipper3d/__pycache__ +find ${PWD}/site/_klipper3d ! -name '*.css' -type f -delete rm ${PWD}/site/sitemap.xml ${PWD}/site/sitemap.xml.gz # Build each additional language website @@ -88,7 +89,8 @@ while IFS="," read dirname langsite langdesc langsearch; do mkdocs build -f "${new_mkdocs_file}" # Cleanup files (mkdocs copies _klipper3d dir and its sitemap doesn't work) - rm -r "${PWD}/site/${langsite}/_klipper3d" + rm -rf "${PWD}/site/${langsite}/_klipper3d/__pycache__" + find "${PWD}/site/${langsite}/_klipper3d" ! -name '*.css' -type f -delete rm "${PWD}/site/${langsite}/sitemap.xml" "${PWD}/site/${langsite}/sitemap.xml.gz" done < <(egrep -v '^ *(#|$)' ${TRANS_FILE}) From 2cc360894536ef3e902acaabc9a495a27d268a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20K=C3=BCffner?= <11882946+mkuf@users.noreply.github.com> Date: Tue, 30 Dec 2025 16:15:35 +0100 Subject: [PATCH 023/108] buildcommands: retrieve version from klippy version file (#7134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Markus Küffner --- scripts/buildcommands.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/buildcommands.py b/scripts/buildcommands.py index b35873840..1e2ac1041 100644 --- a/scripts/buildcommands.py +++ b/scripts/buildcommands.py @@ -483,11 +483,22 @@ def git_version(): logging.debug("Got git version: %s" % (repr(ver),)) return ver +# Obtain version info from "klippy/.version" file +def file_version(): + if not os.path.exists('klippy/.version'): + logging.debug("No 'klippy/.version' file/directory found") + return "" + ver = check_output("cat klippy/.version").strip() + logging.debug("Got klippy version: %s" % (repr(ver),)) + return ver + def build_version(extra, cleanbuild): version = git_version() if not version: cleanbuild = False - version = "?" + version = file_version() + if not version: + version = "?" elif 'dirty' in version: cleanbuild = False if not cleanbuild: From 12cc944fa0c56adf5f31b788efd5e2cc5f4a5094 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 15 Oct 2025 03:06:31 +0200 Subject: [PATCH 024/108] stm32: F042 define PB4 HW PWM Signed-off-by: Timofey Titovets --- src/stm32/Kconfig | 2 +- src/stm32/hard_pwm.c | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index 72be8a4d1..c8466df38 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -10,7 +10,7 @@ config STM32_SELECT select HAVE_GPIO_I2C if !MACH_STM32F031 select HAVE_GPIO_SPI if !MACH_STM32F031 select HAVE_GPIO_SDIO if MACH_STM32F4 - select HAVE_GPIO_HARD_PWM if MACH_STM32F070 || MACH_STM32F072 || MACH_STM32F1 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32G0 || MACH_STM32H7 + select HAVE_GPIO_HARD_PWM if MACH_STM32F042 || MACH_STM32F070 || MACH_STM32F072 || MACH_STM32F1 || MACH_STM32F4 || MACH_STM32F7 || MACH_STM32G0 || MACH_STM32H7 select HAVE_STRICT_TIMING select HAVE_CHIPID select HAVE_STEPPER_OPTIMIZED_BOTH_EDGE if !MACH_STM32H7 diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 6ed27a305..0729d47b1 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -21,6 +21,9 @@ struct gpio_pwm_info { static const struct gpio_pwm_info pwm_regs[] = { #if CONFIG_MACH_STM32F0 + #if CONFIG_MACH_STM32F042 + {TIM3, GPIO('B', 4), 1, GPIO_FUNCTION(1)}, + #endif #if CONFIG_MACH_STM32F070 {TIM15, GPIO('A', 2), 1, GPIO_FUNCTION(0)}, {TIM15, GPIO('A', 3), 2, GPIO_FUNCTION(0)}, From ec0eb4a8bf69249a4ab8aa88a23796eb8e76a090 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 15 Oct 2025 04:25:43 +0200 Subject: [PATCH 025/108] stm32: hard pwm allow scale PWM frequency To support the cartographer, it is required to output 24 MHz. With current defaults max output frequency is: 48 MHz/256 = 187.5 KHz Adjusting the PWM scale allows for ramping up the frequency. To not mess up with existing PWM users, define the STM32-specific command. Signed-off-by: Timofey Titovets --- src/stm32/hard_pwm.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 0729d47b1..8a9dcbb4b 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -300,7 +300,8 @@ static const struct gpio_pwm_info pwm_regs[] = { }; struct gpio_pwm -gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) +gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, + int is_clock_out) { // Find pin in pwm_regs table const struct gpio_pwm_info* p = pwm_regs; @@ -316,7 +317,18 @@ gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) uint32_t pclock_div = CONFIG_CLOCK_FREQ / pclk; if (pclock_div > 1) pclock_div /= 2; // Timers run at twice the normal pclock frequency - uint32_t prescaler = cycle_time / (pclock_div * (MAX_PWM - 1)); + uint32_t max_pwm = MAX_PWM; + uint32_t pcycle_time = cycle_time / pclock_div; + uint32_t prescaler = pcycle_time / (max_pwm - 1); + // CLK output + if (is_clock_out) { + prescaler = 1; + while (pcycle_time > UINT16_MAX) { + prescaler = prescaler * 2; + pcycle_time /= 2; + } + max_pwm = pcycle_time; + } if (prescaler > UINT16_MAX) { prescaler = UINT16_MAX; } else if (prescaler > 0) { @@ -334,9 +346,12 @@ gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) if (p->timer->PSC != (uint16_t) prescaler) { shutdown("PWM already programmed at different speed"); } + if (p->timer->ARR != (uint16_t) max_pwm - 1) { + shutdown("PWM already programmed with different pulse duration"); + } } else { p->timer->PSC = (uint16_t) prescaler; - p->timer->ARR = MAX_PWM - 1; + p->timer->ARR = max_pwm - 1; p->timer->EGR |= TIM_EGR_UG; } @@ -393,6 +408,19 @@ gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) return channel; } +struct gpio_pwm +gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) { + return gpio_timer_setup(pin, cycle_time, val, 0); +} + +void +command_stm32_timer_output(uint32_t *args) +{ + gpio_timer_setup(args[0], args[1], args[2], 1); +} +DECL_COMMAND(command_stm32_timer_output, + "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu"); + void gpio_pwm_write(struct gpio_pwm g, uint32_t val) { *(volatile uint32_t*) g.reg = val; From 1fdf0ebaf4da3f4149c6d50ce2561f3bbc60ad56 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 16 Oct 2025 19:28:19 +0200 Subject: [PATCH 026/108] static_pwm_clock: define module for stm32 Signed-off-by: Timofey Titovets --- klippy/extras/static_pwm_clock.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 klippy/extras/static_pwm_clock.py diff --git a/klippy/extras/static_pwm_clock.py b/klippy/extras/static_pwm_clock.py new file mode 100644 index 000000000..db62af88b --- /dev/null +++ b/klippy/extras/static_pwm_clock.py @@ -0,0 +1,47 @@ +# Define GPIO as clock output +# +# Copyright (C) 2025 Timofey Titovets +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +import logging + +class PrinterClockOutputPin: + def __init__(self, config): + self.name = config.get_name() + self.printer = config.get_printer() + ppins = self.printer.lookup_object('pins') + mcu_pin = ppins.lookup_pin(config.get('pin'), can_invert=True) + self.mcu = mcu_pin["chip"] + self.pin = mcu_pin["pin"] + self.invert = mcu_pin["invert"] + self.frequency = config.getfloat('frequency', 100, above=(1/0.3), + maxval=520000000) + self.mcu.register_config_callback(self._build_config) + def _build_config(self): + self.mcu.lookup_command( + "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu") + mcu_freq = self.mcu.seconds_to_clock(1.0) + cycle_ticks = int(mcu_freq // self.frequency) + # validate frequency + mcu_freq_rev = int(cycle_ticks * self.frequency) + if mcu_freq_rev != mcu_freq: + msg = """ + # Frequency output must be without remainder, %i != %i + [%s] + frequency = %f + """ % (mcu_freq, mcu_freq_rev, self.name, self.frequency) + raise self.printer.config_error(msg) + value = int(0.5 * cycle_ticks) + if self.invert: + value = cycle_ticks - value + if value/cycle_ticks < 0.4: + logging.warning("[%s] pulse width < 40%%" % (self.name)) + if value/cycle_ticks > 0.6: + logging.warning("[%s] pulse width > 60%%" % (self.name)) + self.mcu.add_config_cmd( + "stm32_timer_output pin=%s cycle_ticks=%d on_ticks=%d" + % (self.pin, cycle_ticks, value)) + +def load_config_prefix(config): + return PrinterClockOutputPin(config) From 3d5f352e242e47a398f07aa7c215dd0c0dc06f52 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 16 Oct 2025 22:52:51 +0200 Subject: [PATCH 027/108] docs: Describe static_pwm_clock section Signed-off-by: Timofey Titovets --- docs/Config_Reference.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 528465e64..b01360adf 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3616,6 +3616,20 @@ pin: # These options are deprecated and should no longer be specified. ``` +### [static_pwm_clock] + +Static configurable output pin (one may define any number of +sections with an "static_pwm_clock" prefix). +Pins configured here will be set up as clock output pins. +Generally used to provide clock input to other hardware on the board. +``` +[static_pwm_clock my_pin] +pin: +# The pin to configure as an output. This parameter must be provided. +#frequency: 100 +# Target output frequency. +``` + ### [pwm_tool] Pulse width modulation digital output pins capable of high speed From 7377da63b7b1ca4b62fb7d843b57224f332b37c7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 30 Dec 2025 18:16:40 -0500 Subject: [PATCH 028/108] stm32: Declare gpio_timer_setup() as static in hard_pwm.c Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 8a9dcbb4b..72c7685bd 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -299,7 +299,7 @@ static const struct gpio_pwm_info pwm_regs[] = { #endif }; -struct gpio_pwm +static struct gpio_pwm gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, int is_clock_out) { From db35e99ea19b8e9b978898f3c19afea97c7b9032 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Dec 2025 13:40:57 -0500 Subject: [PATCH 029/108] tmc: Group code startup functions together in TMCCommandHelper() Code movement only; no code changes. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index aad19cf4d..944bc3e1f 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -323,14 +323,20 @@ class TMCCommandHelper: self.name = config.get_name().split()[-1] self.mcu_tmc = mcu_tmc self.current_helper = current_helper + self.fields = mcu_tmc.get_fields() + self.stepper = None + # Stepper phase tracking + self.mcu_phase_offset = None + # Stepper enable/disable tracking + self.toff = None + self.stepper_enable = self.printer.load_object(config, "stepper_enable") + # DUMP_TMC support + self.read_registers = self.read_translate = None + # Common tmc helpers self.echeck_helper = TMCErrorCheck(config, mcu_tmc) self.record_helper = TMCStallguardDump(config, mcu_tmc) - self.fields = mcu_tmc.get_fields() - self.read_registers = self.read_translate = None - self.toff = None - self.mcu_phase_offset = None - self.stepper = None - self.stepper_enable = self.printer.load_object(config, "stepper_enable") + TMCMicrostepHelper(config, mcu_tmc) + # Register callbacks self.printer.register_event_handler("stepper:sync_mcu_position", self._handle_sync_mcu_pos) self.printer.register_event_handler("stepper:set_sdir_inverted", @@ -339,8 +345,6 @@ class TMCCommandHelper: self._handle_mcu_identify) self.printer.register_event_handler("klippy:connect", self._handle_connect) - # Set microstep config options - TMCMicrostepHelper(config, mcu_tmc) # Register commands gcode = self.printer.lookup_object("gcode") gcode.register_mux_command("SET_TMC_FIELD", "STEPPER", self.name, @@ -468,18 +472,19 @@ class TMCCommandHelper: self.echeck_helper.stop_checks() except self.printer.command_error as e: self.printer.invoke_shutdown(str(e)) - def _handle_mcu_identify(self): - # Lookup stepper object - force_move = self.printer.lookup_object("force_move") - self.stepper = force_move.lookup_stepper(self.stepper_name) - # Note pulse duration and step_both_edge optimizations available - self.stepper.setup_default_pulse_duration(.000000100, True) def _handle_stepper_enable(self, print_time, is_enable): if is_enable: cb = (lambda ev: self._do_enable(print_time)) else: cb = (lambda ev: self._do_disable(print_time)) self.printer.get_reactor().register_callback(cb) + # Initial startup handling + def _handle_mcu_identify(self): + # Lookup stepper object + force_move = self.printer.lookup_object("force_move") + self.stepper = force_move.lookup_stepper(self.stepper_name) + # Note pulse duration and step_both_edge optimizations available + self.stepper.setup_default_pulse_duration(.000000100, True) def _handle_connect(self): # Check if using step on both edges optimization pulse_duration, step_both_edge = self.stepper.get_pulse_duration() From d5ef9247510717f7485fc6bcde41b6178e7d5f87 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Dec 2025 14:03:47 -0500 Subject: [PATCH 030/108] tmc: Simplify TMCCommandHelper() error checking Move shutdown checking from _do_enable() and _dos_disable() to new enable_disable_cb(). Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 62 +++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 944bc3e1f..298b71728 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -442,42 +442,40 @@ class TMCCommandHelper: self.mcu_phase_offset = moff # Stepper enable/disable tracking def _do_enable(self, print_time): - try: - if self.toff is not None: - # Shared enable via comms handling - self.fields.set_field("toff", self.toff) - self._init_registers() - did_reset = self.echeck_helper.start_checks() - if did_reset: - self.mcu_phase_offset = None - # Calculate phase offset + if self.toff is not None: + # Shared enable via comms handling + self.fields.set_field("toff", self.toff) + self._init_registers() + did_reset = self.echeck_helper.start_checks() + if did_reset: + self.mcu_phase_offset = None + # Calculate phase offset + if self.mcu_phase_offset is not None: + return + gcode = self.printer.lookup_object("gcode") + with gcode.get_mutex(): if self.mcu_phase_offset is not None: return - gcode = self.printer.lookup_object("gcode") - with gcode.get_mutex(): - if self.mcu_phase_offset is not None: - return - logging.info("Pausing toolhead to calculate %s phase offset", - self.stepper_name) - self.printer.lookup_object('toolhead').wait_moves() - self._handle_sync_mcu_pos(self.stepper) - except self.printer.command_error as e: - self.printer.invoke_shutdown(str(e)) + logging.info("Pausing toolhead to calculate %s phase offset", + self.stepper_name) + self.printer.lookup_object('toolhead').wait_moves() + self._handle_sync_mcu_pos(self.stepper) def _do_disable(self, print_time): - try: - if self.toff is not None: - val = self.fields.set_field("toff", 0) - reg_name = self.fields.lookup_register("toff") - self.mcu_tmc.set_register(reg_name, val, print_time) - self.echeck_helper.stop_checks() - except self.printer.command_error as e: - self.printer.invoke_shutdown(str(e)) + if self.toff is not None: + val = self.fields.set_field("toff", 0) + reg_name = self.fields.lookup_register("toff") + self.mcu_tmc.set_register(reg_name, val, print_time) + self.echeck_helper.stop_checks() def _handle_stepper_enable(self, print_time, is_enable): - if is_enable: - cb = (lambda ev: self._do_enable(print_time)) - else: - cb = (lambda ev: self._do_disable(print_time)) - self.printer.get_reactor().register_callback(cb) + def enable_disable_cb(eventtime): + try: + if is_enable: + self._do_enable(print_time) + else: + self._do_disable(print_time) + except self.printer.command_error as e: + self.printer.invoke_shutdown(str(e)) + self.printer.get_reactor().register_callback(enable_disable_cb) # Initial startup handling def _handle_mcu_identify(self): # Lookup stepper object From 8ea7be5dd7139215a4b3e06b2512d2c3f88c9b5b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 29 Dec 2025 14:09:38 -0500 Subject: [PATCH 031/108] tmc: Hold a mutex during enable/disable checking It's possible for a motor disable request to occur while processing a previous motor enable. Use a reactor mutex to ensure the two events are processed serially. Signed-off-by: Kevin O'Connor --- klippy/extras/tmc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 298b71728..536308d97 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -330,6 +330,7 @@ class TMCCommandHelper: # Stepper enable/disable tracking self.toff = None self.stepper_enable = self.printer.load_object(config, "stepper_enable") + self.enable_mutex = self.printer.get_reactor().mutex() # DUMP_TMC support self.read_registers = self.read_translate = None # Common tmc helpers @@ -469,10 +470,11 @@ class TMCCommandHelper: def _handle_stepper_enable(self, print_time, is_enable): def enable_disable_cb(eventtime): try: - if is_enable: - self._do_enable(print_time) - else: - self._do_disable(print_time) + with self.enable_mutex: + if is_enable: + self._do_enable(print_time) + else: + self._do_disable(print_time) except self.printer.command_error as e: self.printer.invoke_shutdown(str(e)) self.printer.get_reactor().register_callback(enable_disable_cb) From 51dcb09d1274d1f1b9fb8fae3787f774ce16fbe2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 25 Oct 2025 21:17:57 +0200 Subject: [PATCH 032/108] probe_eddy_current: reload z_offset probe helper Currently, there is no way to adjust the calibration curve. The existing z_offset infrastructure is not applicable or disabled here. To make it possible to fine tune calibration curve. Reload Z_OFFSET helper for probe_eddy_current. Signed-off-by: Timofey Titovets --- klippy/extras/probe.py | 5 ++++- klippy/extras/probe_eddy_current.py | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index bfeb33cea..d8f086265 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -36,7 +36,8 @@ def calc_probe_z_average(positions, method='average'): # Helper to implement common probing commands class ProbeCommandHelper: - def __init__(self, config, probe, query_endstop=None): + def __init__(self, config, probe, query_endstop=None, + replace_z_offset=False): self.printer = config.get_printer() self.probe = probe self.query_endstop = query_endstop @@ -57,6 +58,8 @@ class ProbeCommandHelper: # Other commands gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) + if replace_z_offset: + return gcode.register_command('Z_OFFSET_APPLY_PROBE', self.cmd_Z_OFFSET_APPLY_PROBE, desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 779a904fb..b5c4b2897 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -31,6 +31,9 @@ class EddyCalibration: gcode.register_mux_command("PROBE_EDDY_CURRENT_CALIBRATE", "CHIP", cname, self.cmd_EDDY_CALIBRATE, desc=self.cmd_EDDY_CALIBRATE_help) + gcode.register_command('Z_OFFSET_APPLY_PROBE', + self.cmd_Z_OFFSET_APPLY_PROBE, + desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) def is_calibrated(self): return len(self.cal_freqs) > 2 def load_calibration(self, cal): @@ -236,13 +239,16 @@ class EddyCalibration: if len(filtered) <= 8: raise self.printer.command_error( "Failed calibration - No usable data") + z_freq_pairs = [(pos, freq) for pos, freq, _, _ in filtered] + self._save_calibration(z_freq_pairs) + def _save_calibration(self, z_freq_pairs): gcode = self.printer.lookup_object("gcode") gcode.respond_info( "The SAVE_CONFIG command will update the printer config file\n" "and restart the printer.") # Save results cal_contents = [] - for i, (pos, freq, _, _) in enumerate(filtered): + for i, (pos, freq) in enumerate(z_freq_pairs): if not i % 3: cal_contents.append('\n') cal_contents.append("%.6f:%.3f" % (pos, freq)) @@ -256,6 +262,17 @@ class EddyCalibration: # Start manual probe manual_probe.ManualProbeHelper(self.printer, gcmd, self.post_manual_probe) + cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset" + def cmd_Z_OFFSET_APPLY_PROBE(self, gcmd): + gcode_move = self.printer.lookup_object("gcode_move") + offset = gcode_move.get_status()['homing_origin'].z + if offset == 0: + gcmd.respond_info("Nothing to do: Z Offset is 0") + return + cal_zpos = [z - offset for z in self.cal_zpos] + z_freq_pairs = zip(cal_zpos, self.cal_freqs) + z_freq_pairs = sorted(z_freq_pairs) + self._save_calibration(z_freq_pairs) def register_drift_compensation(self, comp): self.drift_comp = comp @@ -519,7 +536,8 @@ class PrinterEddyProbe: self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( config, self.sensor_helper, self.calibration, self.param_helper) - self.cmd_helper = probe.ProbeCommandHelper(config, self) + self.cmd_helper = probe.ProbeCommandHelper(config, self, + replace_z_offset=True) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) From dd625933f7b9bd53363fe015c62aaa874021fa9a Mon Sep 17 00:00:00 2001 From: Kyoungkyu Park Date: Fri, 19 Dec 2025 02:47:27 +0900 Subject: [PATCH 033/108] avr: add lgt8f328p support Signed-off-by: Kyoungkyu Park --- src/avr/Kconfig | 16 ++++++++++++++-- src/avr/Makefile | 2 +- src/avr/adc.c | 27 ++++++++++++++++++++++++++- src/avr/gpio.c | 6 ++++++ src/avr/hard_pwm.c | 3 ++- src/avr/i2c.c | 3 ++- src/avr/spi.c | 3 ++- test/configs/lgt8f328p.config | 15 +++++++++++++++ 8 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 test/configs/lgt8f328p.config diff --git a/src/avr/Kconfig b/src/avr/Kconfig index d4d78d279..74271ec82 100644 --- a/src/avr/Kconfig +++ b/src/avr/Kconfig @@ -11,7 +11,7 @@ config AVR_SELECT select HAVE_GPIO_I2C select HAVE_GPIO_HARD_PWM select HAVE_STRICT_TIMING - select HAVE_LIMITED_CODE_SIZE if MACH_atmega168 || MACH_atmega328 || MACH_atmega328p || MACH_atmega32u4 + select HAVE_LIMITED_CODE_SIZE if MACH_atmega168 || MACH_atmega328 || MACH_atmega328p || MACH_atmega32u4 || MACH_lgt8f328p config BOARD_DIRECTORY string @@ -39,6 +39,8 @@ choice bool "atmega328" config MACH_atmega168 bool "atmega168" + config MACH_lgt8f328p + bool "lgt8f328p" endchoice config MCU @@ -53,6 +55,12 @@ config MCU default "atmega32u4" if MACH_atmega32u4 default "atmega1280" if MACH_atmega1280 default "atmega2560" if MACH_atmega2560 + default "lgt8f328p" if MACH_lgt8f328p + +config AVR_BUILD_MCU + string + default MCU if !MACH_lgt8f328p + default "atmega328p" if MACH_lgt8f328p config AVRDUDE_PROTOCOL string @@ -62,6 +70,9 @@ config AVRDUDE_PROTOCOL choice prompt "Processor speed" if LOW_LEVEL_OPTIONS + config AVR_FREQ_32000000 + bool "32Mhz" + depends on MACH_lgt8f328p config AVR_FREQ_16000000 bool "16Mhz" config AVR_FREQ_20000000 @@ -75,11 +86,12 @@ config CLOCK_FREQ int default 8000000 if AVR_FREQ_8000000 default 20000000 if AVR_FREQ_20000000 + default 32000000 if AVR_FREQ_32000000 default 16000000 config CLEAR_PRESCALER bool "Manually clear the CPU prescaler field at startup" if LOW_LEVEL_OPTIONS - depends on MACH_at90usb1286 || MACH_at90usb646 || MACH_atmega32u4 + depends on MACH_at90usb1286 || MACH_at90usb646 || MACH_atmega32u4 || MACH_lgt8f328p default y help Some AVR chips ship with a "clock prescaler" that causes the diff --git a/src/avr/Makefile b/src/avr/Makefile index f667de3b7..7cf6d68f2 100644 --- a/src/avr/Makefile +++ b/src/avr/Makefile @@ -6,7 +6,7 @@ CROSS_PREFIX=avr- dirs-y += src/avr src/generic CFLAGS-$(CONFIG_HAVE_LIMITED_CODE_SIZE) += -Os -CFLAGS += $(CFLAGS-y) -mmcu=$(CONFIG_MCU) +CFLAGS += $(CFLAGS-y) -mmcu=$(CONFIG_AVR_BUILD_MCU) # Add avr source files src-y += avr/main.c avr/timer.c diff --git a/src/avr/adc.c b/src/avr/adc.c index 1d16368d0..99fd063f6 100644 --- a/src/avr/adc.c +++ b/src/avr/adc.c @@ -30,6 +30,10 @@ static const uint8_t adc_pins[] PROGMEM = { GPIO('F', 4), GPIO('F', 5), GPIO('F', 6), GPIO('F', 7), GPIO('K', 0), GPIO('K', 1), GPIO('K', 2), GPIO('K', 3), GPIO('K', 4), GPIO('K', 5), GPIO('K', 6), GPIO('K', 7), +#elif CONFIG_MACH_lgt8f328p + GPIO('C', 0), GPIO('C', 1), GPIO('C', 2), GPIO('C', 3), + GPIO('C', 4), GPIO('C', 5), GPIO('E', 1), GPIO('E', 3), + GPIO('C', 7), GPIO('F', 0), GPIO('E', 6), GPIO('E', 7), #endif }; @@ -41,7 +45,11 @@ DECL_ENUMERATION_RANGE("pin", "PE2", GPIO('E', 2), 2); enum { ADMUX_DEFAULT = 0x40 }; enum { ADC_ENABLE = (1<= 8) DIDR2 |= 1 << (chan & 0x07); else +#elif CONFIG_MACH_lgt8f328p + if (chan >= 8) + switch (chan) { + case 8: + DIDR1 |= (1 << 2); + break; + case 9: + DIDR1 |= (1 << 3); + break; + case 10: + DIDR1 |= (1 << 6); + break; + case 11: + DIDR1 |= (1 << 7); + break; + } + else #endif DIDR0 |= 1 << chan; diff --git a/src/avr/gpio.c b/src/avr/gpio.c index f52251770..6164a49d5 100644 --- a/src/avr/gpio.c +++ b/src/avr/gpio.c @@ -20,6 +20,9 @@ DECL_ENUMERATION_RANGE("pin", "PC0", GPIO('C', 0), 8); DECL_ENUMERATION_RANGE("pin", "PD0", GPIO('D', 0), 8); #if CONFIG_MACH_atmega328p DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 8); +#elif CONFIG_MACH_lgt8f328p +DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 8); +DECL_ENUMERATION_RANGE("pin", "PF0", GPIO('F', 0), 8); #endif #ifdef PINE DECL_ENUMERATION_RANGE("pin", "PE0", GPIO('E', 0), 8); @@ -42,6 +45,9 @@ volatile uint8_t * const digital_regs[] PROGMEM = { &PINB, &PINC, &PIND, #if CONFIG_MACH_atmega328p &_SFR_IO8(0x0C), // PINE on atmega328pb +#elif CONFIG_MACH_lgt8f328p + &_SFR_IO8(0x0C), // lgt8f328p have PINE and PINF + &_SFR_IO8(0x12) #endif #ifdef PINE &PINE, &PINF, diff --git a/src/avr/hard_pwm.c b/src/avr/hard_pwm.c index 01d0bd9e6..d7cf4c015 100644 --- a/src/avr/hard_pwm.c +++ b/src/avr/hard_pwm.c @@ -22,7 +22,8 @@ struct gpio_pwm_info { enum { GP_8BIT=1, GP_AFMT=2 }; static const struct gpio_pwm_info pwm_regs[] PROGMEM = { -#if CONFIG_MACH_atmega168 || CONFIG_MACH_atmega328 || CONFIG_MACH_atmega328p +#if CONFIG_MACH_atmega168 || CONFIG_MACH_atmega328 \ + || CONFIG_MACH_atmega328p || CONFIG_MACH_lgt8f328p { GPIO('D', 6), &OCR0A, &TCCR0A, &TCCR0B, 1< Date: Sun, 16 Nov 2025 17:14:31 -0500 Subject: [PATCH 034/108] stm32: Enable MOE bit in hard_pwm.c for all stm32 chips Always enable the MOE bit. Reported by @tt33415366. Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 72c7685bd..f4c8cdfde 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -400,11 +400,14 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, default: shutdown("Invalid PWM channel"); } + // Enable PWM output p->timer->CR1 |= TIM_CR1_CEN; -#if CONFIG_MACH_STM32H7 || CONFIG_MACH_STM32G0 - p->timer->BDTR |= TIM_BDTR_MOE; -#endif + + // Advanced timers need MOE enabled. On standard timers this is a + // write to reserved memory, but that seems harmless in practice. + p->timer->BDTR = TIM_BDTR_MOE; + return channel; } From 05ce95645638c962a0d82fb8152d2819224a23af Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 31 Dec 2025 12:39:02 +0100 Subject: [PATCH 035/108] stm32: fix static clock output for faster chips If pcycle_time is scaled, the value should be scaled as well Signed-off-by: Timofey Titovets --- src/stm32/hard_pwm.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index f4c8cdfde..d6ea6dfca 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -323,9 +323,11 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // CLK output if (is_clock_out) { prescaler = 1; + val = val / pclock_div; while (pcycle_time > UINT16_MAX) { prescaler = prescaler * 2; pcycle_time /= 2; + val /= 2; } max_pwm = pcycle_time; } From 8dd798ebb8ad37dcfd6d6d825ff7a7482f3ddafe Mon Sep 17 00:00:00 2001 From: MRX8024 <57844100+MRX8024@users.noreply.github.com> Date: Thu, 1 Jan 2026 00:30:16 +0200 Subject: [PATCH 036/108] tmc: Fix stepper:set_dir_inverted event handler name The event handler is registered with an incorrect event name, causing the handler to never be called. Signed-off-by: Maksim Bolgov maksim8024@gmail.com --- klippy/extras/tmc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/tmc.py b/klippy/extras/tmc.py index 536308d97..23b05305e 100644 --- a/klippy/extras/tmc.py +++ b/klippy/extras/tmc.py @@ -340,7 +340,7 @@ class TMCCommandHelper: # Register callbacks self.printer.register_event_handler("stepper:sync_mcu_position", self._handle_sync_mcu_pos) - self.printer.register_event_handler("stepper:set_sdir_inverted", + self.printer.register_event_handler("stepper:set_dir_inverted", self._handle_sync_mcu_pos) self.printer.register_event_handler("klippy:mcu_identify", self._handle_mcu_identify) From 74a15f4834103956388650705cb88687781b974a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 31 Dec 2025 14:45:19 -0500 Subject: [PATCH 037/108] stm32: Minor comment and code organization changes to hard_pwm.c Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 50 +++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index d6ea6dfca..0b4ea6eb3 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -311,52 +311,58 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, if (p->pin == pin) break; } + gpio_peripheral(p->pin, p->function, 0); // Map cycle_time to pwm clock divisor uint32_t pclk = get_pclock_frequency((uint32_t)p->timer); uint32_t pclock_div = CONFIG_CLOCK_FREQ / pclk; if (pclock_div > 1) pclock_div /= 2; // Timers run at twice the normal pclock frequency - uint32_t max_pwm = MAX_PWM; uint32_t pcycle_time = cycle_time / pclock_div; - uint32_t prescaler = pcycle_time / (max_pwm - 1); - // CLK output - if (is_clock_out) { - prescaler = 1; + + // Convert requested cycle time (cycle_time/CLOCK_FREQ) to actual + // cycle time (hwpwm_ticks*prescaler*pclock_div/CLOCK_FREQ). + uint32_t hwpwm_ticks, prescaler; + if (!is_clock_out) { + // In normal mode, allow the pulse frequency (cycle_time) to change + // in order to maintain the requested duty ratio (val/MAX_PWM). + hwpwm_ticks = MAX_PWM; + prescaler = pcycle_time / (hwpwm_ticks - 1); + if (prescaler > UINT16_MAX + 1) + prescaler = UINT16_MAX + 1; + else if (prescaler < 1) + prescaler = 1; + } else { + // In clock output mode, allow the pulse width enable duration + // (val) to change in order to maintain the requested frequency. val = val / pclock_div; - while (pcycle_time > UINT16_MAX) { - prescaler = prescaler * 2; - pcycle_time /= 2; + hwpwm_ticks = pcycle_time; + prescaler = 1; + while (hwpwm_ticks > UINT16_MAX) { val /= 2; + hwpwm_ticks /= 2; + prescaler *= 2; } - max_pwm = pcycle_time; - } - if (prescaler > UINT16_MAX) { - prescaler = UINT16_MAX; - } else if (prescaler > 0) { - prescaler -= 1; } - gpio_peripheral(p->pin, p->function, 0); - - // Enable clock + // Enable requested pwm hardware block if (!is_enabled_pclock((uint32_t) p->timer)) { enable_pclock((uint32_t) p->timer); } - if (p->timer->CR1 & TIM_CR1_CEN) { - if (p->timer->PSC != (uint16_t) prescaler) { + if (p->timer->PSC != (uint16_t) (prescaler - 1)) { shutdown("PWM already programmed at different speed"); } - if (p->timer->ARR != (uint16_t) max_pwm - 1) { + if (p->timer->ARR != (uint16_t) (hwpwm_ticks - 1)) { shutdown("PWM already programmed with different pulse duration"); } } else { - p->timer->PSC = (uint16_t) prescaler; - p->timer->ARR = max_pwm - 1; + p->timer->PSC = prescaler - 1; + p->timer->ARR = hwpwm_ticks - 1; p->timer->EGR |= TIM_EGR_UG; } + // Enable requested channel of hardware pwm block struct gpio_pwm channel; switch (p->channel) { case 1: { From e60fe3d99b545d7e42ff2f5278efa5822668a57c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 31 Dec 2025 18:39:23 -0500 Subject: [PATCH 038/108] stm32: Fix off-by-one error in the prescaler calculation in hard_pwm.c Signed-off-by: Kevin O'Connor --- src/stm32/hard_pwm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index 0b4ea6eb3..f34e62b00 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -11,7 +11,7 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown -#define MAX_PWM (256 + 1) +#define MAX_PWM 256 DECL_CONSTANT("PWM_MAX", MAX_PWM); struct gpio_pwm_info { @@ -327,7 +327,7 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // In normal mode, allow the pulse frequency (cycle_time) to change // in order to maintain the requested duty ratio (val/MAX_PWM). hwpwm_ticks = MAX_PWM; - prescaler = pcycle_time / (hwpwm_ticks - 1); + prescaler = pcycle_time / MAX_PWM; if (prescaler > UINT16_MAX + 1) prescaler = UINT16_MAX + 1; else if (prescaler < 1) From abda66d6efafdcd12fb423e72cda1e936f6ac226 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 2 Jan 2026 23:44:43 +0100 Subject: [PATCH 039/108] ldc1612: enable frequency div to reduce noise BTT Eddy uses 12MHz clock in frequency. Coil is oscillating at 3+MHz. Which is out of spec for LDC1612 sensors. Division of coil frequency seems to reduce output noise. Signed-off-by: Timofey Titovets --- klippy/extras/ldc1612.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 973556af1..fea4eba9c 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -87,8 +87,12 @@ class LDC1612: self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None - self.frequency = config.getint("frequency", DEFAULT_LDC1612_FREQ, - 2000000, 40000000) + self.clock_freq = config.getint("frequency", DEFAULT_LDC1612_FREQ, + 2000000, 40000000) + # Coil frequency divider, assume 12MHz is BTT Eddy + # BTT Eddy's coil frequency is > 1/4 of reference clock + self.sensor_div = 1 if self.clock_freq != DEFAULT_LDC1612_FREQ else 2 + self.freq_conv = float(self.clock_freq * self.sensor_div) / (1<<28) if config.get('intb_pin', None) is not None: ppins = config.get_printer().lookup_object("pins") pin_params = ppins.lookup_pin(config.get('intb_pin')) @@ -143,7 +147,7 @@ class LDC1612: def setup_home(self, print_time, trigger_freq, trsync_oid, hit_reason, err_reason): clock = self.mcu.print_time_to_clock(print_time) - tfreq = int(trigger_freq * (1<<28) / float(self.frequency) + 0.5) + tfreq = int(trigger_freq / self.freq_conv + 0.5) self.ldc1612_setup_home_cmd.send( [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) def clear_home(self): @@ -155,7 +159,7 @@ class LDC1612: return self.mcu.clock_to_print_time(tclock) # Measurement decoding def _convert_samples(self, samples): - freq_conv = float(self.frequency) / (1<<28) + freq_conv = self.freq_conv count = 0 for ptime, val in samples: mv = val & 0x0fffffff @@ -176,11 +180,11 @@ class LDC1612: "(e.g. faulty wiring) or a faulty ldc1612 chip." % (manuf_id, dev_id, LDC1612_MANUF_ID, LDC1612_DEV_ID)) # Setup chip in requested query rate - rcount0 = self.frequency / (16. * self.data_rate) + rcount0 = self.clock_freq / (16. * self.data_rate) self.set_reg(REG_RCOUNT0, int(rcount0 + 0.5)) self.set_reg(REG_OFFSET0, 0) - self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.frequency/16. + .5)) - self.set_reg(REG_CLOCK_DIVIDERS0, (1 << 12) | 1) + self.set_reg(REG_SETTLECOUNT0, int(SETTLETIME*self.clock_freq/16. + .5)) + self.set_reg(REG_CLOCK_DIVIDERS0, (self.sensor_div << 12) | 1) self.set_reg(REG_ERROR_CONFIG, (0x1f << 11) | 1) self.set_reg(REG_MUX_CONFIG, 0x0208 | DEGLITCH) self.set_reg(REG_CONFIG, 0x001 | (1<<12) | (1<<10) | (1<<9)) From 8bca4cbcd94341010bda1f965cd464c8100a87dd Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 2 Jan 2026 18:33:22 -0500 Subject: [PATCH 040/108] static_pwm_clock: Don't rely on custom stm32_timer_output mcu code Use the regular hardware pwm interface instead of relying on a custom interface. Signed-off-by: Kevin O'Connor --- klippy/extras/static_pwm_clock.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/klippy/extras/static_pwm_clock.py b/klippy/extras/static_pwm_clock.py index db62af88b..e9c018c00 100644 --- a/klippy/extras/static_pwm_clock.py +++ b/klippy/extras/static_pwm_clock.py @@ -11,16 +11,15 @@ class PrinterClockOutputPin: self.name = config.get_name() self.printer = config.get_printer() ppins = self.printer.lookup_object('pins') - mcu_pin = ppins.lookup_pin(config.get('pin'), can_invert=True) - self.mcu = mcu_pin["chip"] - self.pin = mcu_pin["pin"] - self.invert = mcu_pin["invert"] + self.mcu_pin = ppins.setup_pin('pwm', config.get('pin')) + self.mcu = self.mcu_pin.get_mcu() self.frequency = config.getfloat('frequency', 100, above=(1/0.3), maxval=520000000) + self.mcu_pin.setup_cycle_time(1. / self.frequency, True) + self.mcu_pin.setup_max_duration(0.) + self.mcu_pin.setup_start_value(0.5, 0.5) self.mcu.register_config_callback(self._build_config) def _build_config(self): - self.mcu.lookup_command( - "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu") mcu_freq = self.mcu.seconds_to_clock(1.0) cycle_ticks = int(mcu_freq // self.frequency) # validate frequency @@ -33,15 +32,10 @@ class PrinterClockOutputPin: """ % (mcu_freq, mcu_freq_rev, self.name, self.frequency) raise self.printer.config_error(msg) value = int(0.5 * cycle_ticks) - if self.invert: - value = cycle_ticks - value if value/cycle_ticks < 0.4: logging.warning("[%s] pulse width < 40%%" % (self.name)) if value/cycle_ticks > 0.6: logging.warning("[%s] pulse width > 60%%" % (self.name)) - self.mcu.add_config_cmd( - "stm32_timer_output pin=%s cycle_ticks=%d on_ticks=%d" - % (self.pin, cycle_ticks, value)) def load_config_prefix(config): return PrinterClockOutputPin(config) From e605fd18560fbb5a7413ca12b72325ad4e18de16 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 2 Jan 2026 18:37:43 -0500 Subject: [PATCH 041/108] stm32: Improve accuracy of hardware pwm cycle time Use a different method of setting the hardware pwm registers so that the actual cycle_time is much closer to the requested cycle_time. Also, remove the now unused stm32_timer_output command, as the main hardware pwm interface provides the same accuracy. Signed-off-by: Kevin O'Connor --- src/stm32/gpio.h | 3 ++- src/stm32/hard_pwm.c | 56 ++++++++++++++------------------------------ 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/src/stm32/gpio.h b/src/stm32/gpio.h index 0bfffc4b4..d4c565d3c 100644 --- a/src/stm32/gpio.h +++ b/src/stm32/gpio.h @@ -25,7 +25,8 @@ void gpio_in_reset(struct gpio_in g, int32_t pull_up); uint8_t gpio_in_read(struct gpio_in g); struct gpio_pwm { - void *reg; + void *reg; + uint32_t hwpwm_ticks; }; struct gpio_pwm gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val); void gpio_pwm_write(struct gpio_pwm g, uint32_t val); diff --git a/src/stm32/hard_pwm.c b/src/stm32/hard_pwm.c index f34e62b00..08defce22 100644 --- a/src/stm32/hard_pwm.c +++ b/src/stm32/hard_pwm.c @@ -11,7 +11,7 @@ #include "internal.h" // GPIO #include "sched.h" // sched_shutdown -#define MAX_PWM 256 +#define MAX_PWM (1<<15) DECL_CONSTANT("PWM_MAX", MAX_PWM); struct gpio_pwm_info { @@ -299,9 +299,8 @@ static const struct gpio_pwm_info pwm_regs[] = { #endif }; -static struct gpio_pwm -gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, - int is_clock_out) +struct gpio_pwm +gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) { // Find pin in pwm_regs table const struct gpio_pwm_info* p = pwm_regs; @@ -322,28 +321,18 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // Convert requested cycle time (cycle_time/CLOCK_FREQ) to actual // cycle time (hwpwm_ticks*prescaler*pclock_div/CLOCK_FREQ). - uint32_t hwpwm_ticks, prescaler; - if (!is_clock_out) { - // In normal mode, allow the pulse frequency (cycle_time) to change - // in order to maintain the requested duty ratio (val/MAX_PWM). - hwpwm_ticks = MAX_PWM; - prescaler = pcycle_time / MAX_PWM; - if (prescaler > UINT16_MAX + 1) - prescaler = UINT16_MAX + 1; - else if (prescaler < 1) - prescaler = 1; - } else { - // In clock output mode, allow the pulse width enable duration - // (val) to change in order to maintain the requested frequency. - val = val / pclock_div; - hwpwm_ticks = pcycle_time; - prescaler = 1; - while (hwpwm_ticks > UINT16_MAX) { - val /= 2; - hwpwm_ticks /= 2; - prescaler *= 2; - } + uint32_t hwpwm_ticks = pcycle_time, prescaler = 1, shift = 0; + while (hwpwm_ticks > UINT16_MAX) { + shift += 1; + hwpwm_ticks = (pcycle_time + (1 << (shift-1))) >> shift; + prescaler = 1 << shift; } + if (prescaler > UINT16_MAX + 1) { + prescaler = UINT16_MAX + 1; + hwpwm_ticks = UINT16_MAX; + } + if (hwpwm_ticks < 2) + hwpwm_ticks = 2; // Enable requested pwm hardware block if (!is_enabled_pclock((uint32_t) p->timer)) { @@ -364,6 +353,7 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, // Enable requested channel of hardware pwm block struct gpio_pwm channel; + channel.hwpwm_ticks = hwpwm_ticks; switch (p->channel) { case 1: { channel.reg = (void*) &p->timer->CCR1; @@ -419,20 +409,8 @@ gpio_timer_setup(uint8_t pin, uint32_t cycle_time, uint32_t val, return channel; } -struct gpio_pwm -gpio_pwm_setup(uint8_t pin, uint32_t cycle_time, uint32_t val) { - return gpio_timer_setup(pin, cycle_time, val, 0); -} - -void -command_stm32_timer_output(uint32_t *args) -{ - gpio_timer_setup(args[0], args[1], args[2], 1); -} -DECL_COMMAND(command_stm32_timer_output, - "stm32_timer_output pin=%u cycle_ticks=%u on_ticks=%hu"); - void gpio_pwm_write(struct gpio_pwm g, uint32_t val) { - *(volatile uint32_t*) g.reg = val; + uint32_t r = DIV_ROUND_CLOSEST(val * g.hwpwm_ticks, MAX_PWM); + *(volatile uint32_t*) g.reg = r; } From f7ddb4003762f3f5baf4164a7125b3db582dab96 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 8 Feb 2025 16:03:27 +0100 Subject: [PATCH 042/108] ldc1612: handle i2c errors I2C error means we don't know the sensor status. Force data output to the host and cancel homing. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 19 ++++++ klippy/extras/probe_eddy_current.py | 4 +- src/sensor_ldc1612.c | 100 ++++++++++++++++++++++------ 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index fea4eba9c..66099b9f8 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -84,6 +84,7 @@ class LDC1612: default_addr=LDC1612_ADDR, default_speed=400000) self.mcu = mcu = self.i2c.get_mcu() + self._sensor_errors = {} self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None @@ -132,6 +133,8 @@ class LDC1612: "query_ldc1612_home_state oid=%c", "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", oid=self.oid, cq=cmdqueue) + errors = self.mcu.get_enumerations().get("ldc1612_error:", {}) + self._sensor_errors = {v: k for k, v in errors.items()} def get_mcu(self): return self.i2c.get_mcu() def read_reg(self, reg): @@ -157,16 +160,32 @@ class LDC1612: params = self.query_ldc1612_home_state_cmd.send([self.oid]) tclock = self.mcu.clock32_to_clock64(params['trigger_clock']) return self.mcu.clock_to_print_time(tclock) + def lookup_sensor_error(self, error): + return self._sensor_errors.get(error, "Unknown ldc1612 error") # Measurement decoding def _convert_samples(self, samples): freq_conv = self.freq_conv count = 0 + errors = {} + def log_once(msg): + if not errors.get(msg, 0): + errors[msg] = 0 + errors[msg] += 1 for ptime, val in samples: mv = val & 0x0fffffff if mv != val: self.last_error_count += 1 + if (val >> 16 & 0xffff) == 0xffff: + # Encoded error from sensor_ldc1612.c + log_once(self.lookup_sensor_error(val & 0xffff)) + continue + error_bits = (val >> 28) & 0x0f + log_once("Sensor reports error (%s)" % (bin(error_bits),)) samples[count] = (round(ptime, 6), round(freq_conv * mv, 3), 999.9) count += 1 + del samples[count:] + for msg in errors: + logging.error("%s: %s (%d)" % (self.name, msg, errors[msg])) # Start, stop, and process message batches def _start_measurements(self): # In case of miswiring, testing LDC1612 device ID prevents treating diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index b5c4b2897..c7b415d02 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -415,7 +415,9 @@ class EddyDescend: if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: raise self._printer.command_error( "Communication timeout during homing") - raise self._printer.command_error("Eddy current sensor error") + error_code = res - self.REASON_SENSOR_ERROR + error_msg = self._sensor_helper.lookup_sensor_error(error_code) + raise self._printer.command_error(error_msg) if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 8b67884f1..e5ab60e3b 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -37,6 +37,16 @@ struct ldc1612 { static struct task_wake ldc1612_wake; +// Internal errors transmitted in sample reports (or trsync error) +enum { + SE_SENSOR_ERROR, SE_I2C_STATUS, SE_I2C_DATA, SE_INVALID_DATA +}; + +DECL_ENUMERATION("ldc1612_error:", "SENSOR_REPORTS_ERROR", SE_SENSOR_ERROR); +DECL_ENUMERATION("ldc1612_error:", "I2C_STATUS_ERROR", SE_I2C_STATUS); +DECL_ENUMERATION("ldc1612_error:", "I2C_DATA_ERROR", SE_I2C_DATA); +DECL_ENUMERATION("ldc1612_error:", "INVALID_READ_DATA", SE_INVALID_DATA); + // Check if the intb line is "asserted" static int check_intb_asserted(struct ldc1612 *ld) @@ -111,19 +121,34 @@ command_query_ldc1612_home_state(uint32_t *args) DECL_COMMAND(command_query_ldc1612_home_state, "query_ldc1612_home_state oid=%c"); +// Cancel homing due to an error +static void +cancel_homing(struct ldc1612 *ld, int error_code) +{ + if (!(ld->homing_flags & LH_CAN_TRIGGER)) + return; + ld->homing_flags = 0; + trsync_do_trigger(ld->ts, ld->error_reason + error_code); +} + +static int +check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { + if (raw_data < 0x0fffffff) + return 0; + cancel_homing(ld, SE_SENSOR_ERROR); + return -1; +} + // Check if a sample should trigger a homing event static void -check_home(struct ldc1612 *ld, uint32_t data) +check_home(struct ldc1612 *ld, uint32_t raw_data) { uint8_t homing_flags = ld->homing_flags; if (!(homing_flags & LH_CAN_TRIGGER)) return; - if (data > 0x0fffffff) { - // Sensor reports an issue - cancel homing - ld->homing_flags = 0; - trsync_do_trigger(ld->ts, ld->error_reason); + if (check_data_bits(ld, raw_data)) return; - } + uint32_t data = raw_data & 0x0fffffff; uint32_t time = timer_read_time(); if ((homing_flags & LH_AWAIT_HOMING) && timer_is_before(time, ld->homing_clock)) @@ -143,41 +168,69 @@ check_home(struct ldc1612 *ld, uint32_t data) #define REG_STATUS 0x18 // Read a register on the ldc1612 -static void +static int read_reg(struct ldc1612 *ld, uint8_t reg, uint8_t *res) { - int ret = i2c_dev_read(ld->i2c, sizeof(reg), ®, 2, res); - i2c_shutdown_on_err(ret); + return i2c_dev_read(ld->i2c, sizeof(reg), ®, 2, res); } // Read the status register on the ldc1612 -static uint16_t -read_reg_status(struct ldc1612 *ld) +static int +read_reg_status(struct ldc1612 *ld, uint16_t *status) { uint8_t data_status[2]; - read_reg(ld, REG_STATUS, data_status); - return (data_status[0] << 8) | data_status[1]; + int ret = read_reg(ld, REG_STATUS, data_status); + *status = (data_status[0] << 8) | data_status[1]; + return ret; } +#define STATUS_UNREADCONV0 (1 << 3) #define BYTES_PER_SAMPLE 4 +static void +report_sample_error(struct ldc1612 *ld, int error_code) +{ + cancel_homing(ld, error_code); + + uint8_t *d = &ld->sb.data[ld->sb.data_count]; + d[0] = 0xff; + d[1] = 0xff; + d[2] = 0; + d[3] = error_code; +} + // Query ldc1612 data static void ldc1612_query(struct ldc1612 *ld, uint8_t oid) { // Check if data available (and clear INTB line) - uint16_t status = read_reg_status(ld); + uint16_t status; + int ret = read_reg_status(ld, &status); irq_disable(); ld->flags &= ~LDC_PENDING; irq_enable(); - if (!(status & 0x08)) + if (ret) { + report_sample_error(ld, SE_I2C_STATUS); + goto out; + } + if (!(status & STATUS_UNREADCONV0)) + // No data available return; // Read coil0 frequency uint8_t *d = &ld->sb.data[ld->sb.data_count]; - read_reg(ld, REG_DATA0_MSB, &d[0]); - read_reg(ld, REG_DATA0_LSB, &d[2]); - ld->sb.data_count += BYTES_PER_SAMPLE; + ret |= read_reg(ld, REG_DATA0_MSB, &d[0]); + ret |= read_reg(ld, REG_DATA0_LSB, &d[2]); + + if (ret) { + report_sample_error(ld, SE_I2C_DATA); + goto out; + } + if (d[0] == 0xff && d[1] == 0xff) { + // Invalid data from sensor (conflict with internal error indicator) + report_sample_error(ld, SE_INVALID_DATA); + goto out; + } // Check for endstop trigger uint32_t data = ((uint32_t)d[0] << 24) @@ -185,7 +238,8 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) | ((uint32_t)d[2] << 8) | ((uint32_t)d[3]); check_home(ld, data); - +out: + ld->sb.data_count += BYTES_PER_SAMPLE; // Flush local buffer if needed if (ld->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ld->sb.data)) sensor_bulk_report(&ld->sb, oid); @@ -228,11 +282,15 @@ command_query_status_ldc1612(uint32_t *args) } // Query sensor to see if a sample is pending + uint16_t status; uint32_t time1 = timer_read_time(); - uint16_t status = read_reg_status(ld); + int ret = read_reg_status(ld, &status); uint32_t time2 = timer_read_time(); - uint32_t fifo = status & 0x08 ? BYTES_PER_SAMPLE : 0; + if (ret) + // Query error - don't send response - host will retry + return; + uint32_t fifo = status & STATUS_UNREADCONV0 ? BYTES_PER_SAMPLE : 0; sensor_bulk_status(&ld->sb, args[0], time1, time2-time1, fifo); } DECL_COMMAND(command_query_status_ldc1612, "query_status_ldc1612 oid=%c"); From c6c76149726d1d7563db3dece7897109da292f43 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 27 Nov 2025 00:54:00 +0100 Subject: [PATCH 043/108] ldc1612: ignore amplitude errors during homing Amplitude errors are useful but often too aggressive. On some sensors, it is not possible to avoid them completely. Make them non-critical for homing. Signed-off-by: Timofey Titovets --- src/sensor_ldc1612.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index e5ab60e3b..7211c2c19 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -131,8 +131,12 @@ cancel_homing(struct ldc1612 *ld, int error_code) trsync_do_trigger(ld->ts, ld->error_reason + error_code); } +#define DATA_ERROR_AMPLITUDE (1L << 28) + static int check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { + // Ignore amplitude errors + raw_data &= ~DATA_ERROR_AMPLITUDE; if (raw_data < 0x0fffffff) return 0; cancel_homing(ld, SE_SENSOR_ERROR); From 2e0d746172bc5ed961083659d2981df02d0a2a8b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 19 Dec 2025 04:51:47 +0100 Subject: [PATCH 044/108] ldc1612: trigger error on high frequency If the sensor coil is disconnected, the frequency is equal to the reference. If the sensor is misconfigured or damaged, the coil frequency is greater than 1/4 of the reference frequency. Signed-off-by: Timofey Titovets --- src/sensor_ldc1612.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 7211c2c19..35c69a2bd 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -131,13 +131,15 @@ cancel_homing(struct ldc1612 *ld, int error_code) trsync_do_trigger(ld->ts, ld->error_reason + error_code); } +#define MAX_VALID_RAW_VALUE 0x03ffffff #define DATA_ERROR_AMPLITUDE (1L << 28) static int check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { // Ignore amplitude errors raw_data &= ~DATA_ERROR_AMPLITUDE; - if (raw_data < 0x0fffffff) + // Datasheet define valid frequency input as < F_ref / 4 + if (raw_data < MAX_VALID_RAW_VALUE) return 0; cancel_homing(ld, SE_SENSOR_ERROR); return -1; From f1bd17d83df35744cb583ddb6633c4ba61824aad Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 27 Nov 2025 00:47:59 +0100 Subject: [PATCH 045/108] ldc1612: decode error flags Most errors, aside from amplitude, should never happen. Output them to the log to simplify later debugging. Count them to aggregate error metrics. Signed-off-by: Timofey Titovets --- klippy/extras/ldc1612.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 66099b9f8..29c8cad2d 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -173,14 +173,22 @@ class LDC1612: errors[msg] += 1 for ptime, val in samples: mv = val & 0x0fffffff - if mv != val: + if val > 0x03ffffff or val == 0x0: self.last_error_count += 1 if (val >> 16 & 0xffff) == 0xffff: # Encoded error from sensor_ldc1612.c log_once(self.lookup_sensor_error(val & 0xffff)) continue error_bits = (val >> 28) & 0x0f - log_once("Sensor reports error (%s)" % (bin(error_bits),)) + if error_bits & 0x8 or mv == 0x0000000: + log_once("Frequency under valid range") + if error_bits & 0x4 or mv > 0x3ffffff: + type = "hard" if error_bits & 0x4 else "soft" + log_once("Frequency over valid %s range" % (type)) + if error_bits & 0x2: + log_once("Conversion Watchdog timeout") + if error_bits & 0x1: + log_once("Amplitude Low/High warning") samples[count] = (round(ptime, 6), round(freq_conv * mv, 3), 999.9) count += 1 del samples[count:] From 1fc9d81095a647401521d3e94bf34e8d4d3a363f Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 4 Jan 2026 22:48:23 +0100 Subject: [PATCH 046/108] docs: describe eddy error messages Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 8d81bd88b..11069c4bc 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -144,3 +144,38 @@ to perform thermal drift calibration: As one may conclude, the calibration process outlined above is more challenging and time consuming than most other procedures. It may require practice and several attempts to achieve an optimal calibration. + +## Errors description + +Possible homing errors and actionables: + +- Sensor error + - Check logs for detailed error +- Eddy I2C STATUS/DATA error. + - Check loose wiring. + - Try software I2C/decrease I2C rate +- Invalid read data + - Same as I2C + +Possible sensor errors and actionables: +- Frequency over valid hard range + - Check frequency configuration + - Hardware fault +- Frequency over valid soft range + - Check frequency configuration +- Conversion Watchdog timeout + - Hardware fault + +Amplitude Low/High warning messages can mean: +- Sensor close to the bed +- Sensor far from the bed +- Higher temperature than was at the current calibration +- Capacitor missing + +On some sensors, it is not possible to completely avoid amplitude +warning indicator. + +You can try to redo the `LDC_CALIBRATE_DRIVE_CURRENT` calibration at work +temperature or increase `reg_drive_current` by 1-2 from the calibrated value. + +Generally, it is like an engine check light. It may indicate an issue. From 2bd0acb6ca4d75e7035c20465a8f36d5e42e2b65 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Fri, 9 Jan 2026 10:57:47 +0100 Subject: [PATCH 047/108] exclude_object: Fixed object exclusion with changing GCode axes Signed-off-by: Dmitry Butyugin --- klippy/extras/exclude_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/exclude_object.py b/klippy/extras/exclude_object.py index c80dd000d..3ca6d53eb 100644 --- a/klippy/extras/exclude_object.py +++ b/klippy/extras/exclude_object.py @@ -89,7 +89,7 @@ class ExcludeObject: offset = [0.] * num_coord self.extrusion_offsets[ename] = offset if len(offset) < num_coord: - offset.extend([0.] * (len(num_coord) - len(offset))) + offset.extend([0.] * (num_coord - len(offset))) return offset def get_position(self): From 48f0b3cad6d4593746384bf49a39913dcb8cc796 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 4 Jan 2026 13:09:28 -0500 Subject: [PATCH 048/108] gcode_move: Export more than 4 components in gcode_position status Commit f04895f5 documented that "{printer.gcode_move.gcode_position}" may contain more than 4 components, however the code was not actually updated to export that additional information. (Commit ac6cab91 only made the change to "homing_origin" and "position".) Export the information in gcode_position as intended. Signed-off-by: Kevin O'Connor --- klippy/extras/gcode_move.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klippy/extras/gcode_move.py b/klippy/extras/gcode_move.py index c2b307663..3980f556e 100644 --- a/klippy/extras/gcode_move.py +++ b/klippy/extras/gcode_move.py @@ -94,7 +94,7 @@ class GCodeMove: def _get_gcode_position(self): p = [lp - bp for lp, bp in zip(self.last_position, self.base_position)] p[3] /= self.extrude_factor - return p[:4] + return p def _get_gcode_speed(self): return self.speed / self.speed_factor def _get_gcode_speed_override(self): @@ -191,7 +191,7 @@ class GCodeMove: def cmd_M114(self, gcmd): # Get Current Position p = self._get_gcode_position() - gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p)) + gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p[:4])) def cmd_M220(self, gcmd): # Set speed factor override percentage value = gcmd.get_float('S', 100., above=0.) / (60. * 100.) From e590bc87d8c6b279f24f592714b25e90673236e7 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 20:27:02 -0500 Subject: [PATCH 049/108] spi_flash: Don't import mcu module Avoid using mcu.CommandQueryWrapper() and mcu.CommandWrapper() classes. Instead, implement local variants of these classes. This will make it easier to modify the mcu classes without fear of breaking the spi_flash code. Signed-off-by: Kevin O'Connor --- scripts/spi_flash/spi_flash.py | 46 ++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py index e9394dbe5..af93b53e9 100644 --- a/scripts/spi_flash/spi_flash.py +++ b/scripts/spi_flash/spi_flash.py @@ -21,7 +21,6 @@ import util import reactor import serialhdl import clocksync -import mcu ########################################################### # @@ -143,11 +142,38 @@ class SPIFlashError(Exception): class MCUConfigError(SPIFlashError): pass +# Wrapper around query commands +class CommandQueryWrapper: + def __init__(self, serial, msgformat, respformat, oid=None): + self._serial = serial + self._cmd = serial.get_msgparser().lookup_command(msgformat) + serial.get_msgparser().lookup_command(respformat) + self._response = respformat.split()[0] + self._oid = oid + self._cmd_queue = serial.get_default_command_queue() + def send(self, data=(), minclock=0, reqclock=0, retry=True): + cmds = [self._cmd.encode(data)] + xh = serialhdl.SerialRetryCommand(self._serial, self._response, + self._oid) + reqclock = max(minclock, reqclock) + return xh.get_response(cmds, self._cmd_queue, minclock, reqclock, retry) + +# Wrapper around command sending +class CommandWrapper: + def __init__(self, serial, msgformat): + self._serial = serial + msgparser = serial.get_msgparser() + self._cmd = msgparser.lookup_command(msgformat) + self._cmd_queue = serial.get_default_command_queue() + def send(self, data=(), minclock=0, reqclock=0): + cmd = self._cmd.encode(data) + self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue) + class SPIDirect: def __init__(self, ser): self.oid = SPI_OID - self._spi_send_cmd = mcu.CommandWrapper(ser, SPI_SEND_CMD) - self._spi_transfer_cmd = mcu.CommandQueryWrapper( + self._spi_send_cmd = CommandWrapper(ser, SPI_SEND_CMD) + self._spi_transfer_cmd = CommandQueryWrapper( ser, SPI_XFER_CMD, SPI_XFER_RESPONSE, self.oid) def spi_send(self, data): @@ -159,18 +185,18 @@ class SPIDirect: class SDIODirect: def __init__(self, ser): self.oid = SDIO_OID - self._sdio_send_cmd = mcu.CommandQueryWrapper( + self._sdio_send_cmd = CommandQueryWrapper( ser, SDIO_SEND_CMD, SDIO_SEND_CMD_RESPONSE, self.oid) - self._sdio_read_data = mcu.CommandQueryWrapper( + self._sdio_read_data = CommandQueryWrapper( ser, SDIO_READ_DATA, SDIO_READ_DATA_RESPONSE, self.oid) - self._sdio_write_data = mcu.CommandQueryWrapper( + self._sdio_write_data = CommandQueryWrapper( ser, SDIO_WRITE_DATA, SDIO_WRITE_DATA_RESPONSE, self.oid) - self._sdio_read_data_buffer = mcu.CommandQueryWrapper( + self._sdio_read_data_buffer = CommandQueryWrapper( ser, SDIO_READ_DATA_BUFFER, SDIO_READ_DATA_BUFFER_RESPONSE, self.oid) - self._sdio_write_data_buffer = mcu.CommandWrapper(ser, + self._sdio_write_data_buffer = CommandWrapper(ser, SDIO_WRITE_DATA_BUFFER) - self._sdio_set_speed = mcu.CommandWrapper(ser, SDIO_SET_SPEED) + self._sdio_set_speed = CommandWrapper(ser, SDIO_SET_SPEED) def sdio_send_cmd(self, cmd, argument, wait): return self._sdio_send_cmd.send([self.oid, cmd, argument, wait]) @@ -1254,7 +1280,7 @@ class MCUConnection: # Iterate through backwards compatible response strings for response in GET_CFG_RESPONSES: try: - get_cfg_cmd = mcu.CommandQueryWrapper( + get_cfg_cmd = CommandQueryWrapper( self._serial, GET_CFG_CMD, response) break except Exception as err: From 1cde5e201869609f685a9e8dbd3e6e49b166193f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 20:40:40 -0500 Subject: [PATCH 050/108] mcu: Pass conn_helper to CommandWrapper and CommandQueryWrapper Pass the low-level MCUConnectHelper class to these helper classes. Signed-off-by: Kevin O'Connor --- klippy/mcu.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/klippy/mcu.py b/klippy/mcu.py index 507f77347..20ffc3fdf 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -60,14 +60,14 @@ class RetryAsyncCommand: # Wrapper around query commands class CommandQueryWrapper: - def __init__(self, serial, msgformat, respformat, oid=None, - cmd_queue=None, is_async=False, error=serialhdl.error): - self._serial = serial + def __init__(self, conn_helper, msgformat, respformat, oid=None, + cmd_queue=None, is_async=False): + self._serial = serial = conn_helper.get_serial() self._cmd = serial.get_msgparser().lookup_command(msgformat) serial.get_msgparser().lookup_command(respformat) self._response = respformat.split()[0] self._oid = oid - self._error = error + self._error = conn_helper.get_mcu().get_printer().command_error self._xmit_helper = serialhdl.SerialRetryCommand if is_async: self._xmit_helper = RetryAsyncCommand @@ -92,15 +92,15 @@ class CommandQueryWrapper: # Wrapper around command sending class CommandWrapper: - def __init__(self, serial, msgformat, cmd_queue=None, debugoutput=False): - self._serial = serial + def __init__(self, conn_helper, msgformat, cmd_queue=None): + self._serial = serial = conn_helper.get_serial() msgparser = serial.get_msgparser() self._cmd = msgparser.lookup_command(msgformat) if cmd_queue is None: cmd_queue = serial.get_default_command_queue() self._cmd_queue = cmd_queue self._msgtag = msgparser.lookup_msgid(msgformat) & 0xffffffff - if debugoutput: + if conn_helper.get_mcu().is_fileoutput(): # Can't use send_wait_ack when in debugging mode self.send_wait_ack = self.send def send(self, data=(), minclock=0, reqclock=0): @@ -1096,12 +1096,11 @@ class MCU: def max_nominal_duration(self): return MAX_NOMINAL_DURATION def lookup_command(self, msgformat, cq=None): - return CommandWrapper(self._serial, msgformat, cq, - debugoutput=self.is_fileoutput()) + return CommandWrapper(self._conn_helper, msgformat, cq) def lookup_query_command(self, msgformat, respformat, oid=None, cq=None, is_async=False): - return CommandQueryWrapper(self._serial, msgformat, respformat, oid, - cq, is_async, self._printer.command_error) + return CommandQueryWrapper(self._conn_helper, msgformat, respformat, + oid, cq, is_async) def try_lookup_command(self, msgformat): try: return self.lookup_command(msgformat) From 57b94520de9e2d21f9381adcf165b73f1fa90ca4 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 21:25:19 -0500 Subject: [PATCH 051/108] mcu: Generate a dummy response to query commands when in debugging mode Previously a querycmd.send() request would silently hang if the code is run in "file output" mode (that is, it is not communicating with a real micro-controller). This behavior makes it hard to implement regression tests and is generally confusing. Change the code to respond with a dummy response (typically all zeros and empty strings) instead. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 4 ++++ klippy/mcu.py | 18 +++++++++++++++++- klippy/msgproto.py | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 16080dc72..5e9ef72ba 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -175,6 +175,8 @@ class ADS1220: # read startup register state and validate val = self.read_reg(0x0, 4) if val != RESET_STATE: + if self.mcu.is_fileoutput(): + return raise self.printer.command_error( "Invalid ads1220 reset state (got %s vs %s).\n" "This is generally indicative of connection problems\n" @@ -209,6 +211,8 @@ class ADS1220: self.spi.spi_send(write_command) stored_val = self.read_reg(reg, len(register_bytes)) if bytearray(register_bytes) != stored_val: + if self.mcu.is_fileoutput(): + return raise self.printer.command_error( "Failed to set ADS1220 register [0x%x] to %s: got %s. " "This may be a connection problem (e.g. faulty wiring)" % ( diff --git a/klippy/mcu.py b/klippy/mcu.py index 20ffc3fdf..909b8ddb5 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -22,6 +22,20 @@ MAX_NOMINAL_DURATION = 3.0 # Command transmit helper classes ###################################################################### +# Generate a dummy response to query commands when in debugging mode +class DummyResponse: + def __init__(self, serial, name, oid=None): + params = {} + if oid is not None: + params['oid'] = oid + msgparser = serial.get_msgparser() + resp = msgparser.create_dummy_response(name, params) + resp['#sent_time'] = 0. + resp['#receive_time'] = 0. + self._response = resp + def get_response(self, cmds, cmd_queue, minclock=0, reqclock=0, retry=True): + return dict(self._response) + # Class to retry sending of a query command until a given response is received class RetryAsyncCommand: TIMEOUT_TIME = 5.0 @@ -69,7 +83,9 @@ class CommandQueryWrapper: self._oid = oid self._error = conn_helper.get_mcu().get_printer().command_error self._xmit_helper = serialhdl.SerialRetryCommand - if is_async: + if conn_helper.get_mcu().is_fileoutput(): + self._xmit_helper = DummyResponse + elif is_async: self._xmit_helper = RetryAsyncCommand if cmd_queue is None: cmd_queue = serial.get_default_command_queue() diff --git a/klippy/msgproto.py b/klippy/msgproto.py index 25701df36..021a025a9 100644 --- a/klippy/msgproto.py +++ b/klippy/msgproto.py @@ -353,6 +353,27 @@ class MessageParser: #logging.exception("Unable to encode") self._error("Unable to encode: %s", msgname) return cmd + def create_dummy_response(self, msgname, params={}): + mp = self.messages_by_name.get(msgname) + if mp is None: + self._error("Unknown response: %s", msgname) + argparts = dict(params) + for name, t in mp.name_to_type.items(): + if name not in argparts: + tval = 0 + if t.is_dynamic_string: + tval = () + argparts[name] = tval + try: + msg = mp.encode_by_name(**argparts) + except error as e: + raise + except: + #logging.exception("Unable to encode") + self._error("Unable to encode: %s", msgname) + res, pos = mp.parse(msg, 0) + res['#name'] = msgname + return res def fill_enumerations(self, enumerations): for add_name, add_enums in enumerations.items(): enums = self.enumerations.setdefault(add_name, {}) From 8be004401e929d5c4e750880f257ab8a7c5a80be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 21:53:27 -0500 Subject: [PATCH 052/108] probe_eddy_current: Implement regression test case Update the code to support simple regression test cases. Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 7 +-- klippy/extras/probe_eddy_current.py | 11 ++-- test/klippy/eddy.cfg | 84 +++++++++++++++++++++++++++++ test/klippy/eddy.test | 30 +++++++++++ 4 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 test/klippy/eddy.cfg create mode 100644 test/klippy/eddy.test diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index 29c8cad2d..db539c650 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -138,6 +138,8 @@ class LDC1612: def get_mcu(self): return self.i2c.get_mcu() def read_reg(self, reg): + if self.mcu.is_fileoutput(): + return 0 params = self.i2c.i2c_read([reg], 2) response = bytearray(params['response']) return (response[0] << 8) | response[1] @@ -155,8 +157,6 @@ class LDC1612: [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) def clear_home(self): self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0]) - if self.mcu.is_fileoutput(): - return 0. params = self.query_ldc1612_home_state_cmd.send([self.oid]) tclock = self.mcu.clock32_to_clock64(params['trigger_clock']) return self.mcu.clock_to_print_time(tclock) @@ -200,7 +200,8 @@ class LDC1612: # noise or wrong signal as a correctly initialized device manuf_id = self.read_reg(REG_MANUFACTURER_ID) dev_id = self.read_reg(REG_DEVICE_ID) - if manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID: + if ((manuf_id != LDC1612_MANUF_ID or dev_id != LDC1612_DEV_ID) + and not self.mcu.is_fileoutput()): raise self.printer.command_error( "Invalid ldc1612 id (got %x,%x vs %x,%x).\n" "This is generally indicative of connection problems\n" diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index c7b415d02..b833f9ab9 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -313,6 +313,13 @@ class EddyGatherSamples: if est_print_time > end_time + 1.0: raise self._printer.command_error( "probe_eddy_current sensor outage") + if mcu.is_fileoutput(): + # In debugging mode + if pos_time is not None: + toolhead_pos = self._lookup_toolhead_pos(pos_time) + self._probe_results.append((toolhead_pos[2], toolhead_pos)) + self._probe_times.pop(0) + continue reactor.pause(systime + 0.010) def _pull_freq(self, start_time, end_time): # Find average sensor frequency between time range @@ -421,7 +428,7 @@ class EddyDescend: if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: return 0. if self._mcu.is_fileoutput(): - return home_end_time + trigger_time = home_end_time self._trigger_time = trigger_time return trigger_time # Probe session interface @@ -437,8 +444,6 @@ class EddyDescend: # Perform probing move phoming = self._printer.lookup_object('homing') trig_pos = phoming.probing_move(self, pos, speed) - if not self._trigger_time: - return trig_pos # Extract samples start_time = self._trigger_time + 0.050 end_time = start_time + 0.100 diff --git a/test/klippy/eddy.cfg b/test/klippy/eddy.cfg new file mode 100644 index 000000000..11946ebe4 --- /dev/null +++ b/test/klippy/eddy.cfg @@ -0,0 +1,84 @@ +# Test config for probe_eddy_current +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[probe_eddy_current eddy] +z_offset: 0.4 +x_offset: -5 +y_offset: -4 +sensor_type: ldc1612 +speed: 10.0 +intb_pin: PK7 + +[bed_mesh] +mesh_min: 10,10 +mesh_max: 180,180 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +# Dummy calibration data +[probe_eddy_current eddy] +calibrate = + 0.050000:3300000.000,1.000000:3200000.000,5.000000:3000000.000 diff --git a/test/klippy/eddy.test b/test/klippy/eddy.test new file mode 100644 index 000000000..5251be122 --- /dev/null +++ b/test/klippy/eddy.test @@ -0,0 +1,30 @@ +# Test case for probe_eddy_current support +CONFIG eddy.cfg +DICTIONARY atmega2560.dict + +# Start by homing the printer. +G28 +G1 F6000 + +# Z / X / Y moves +G1 Z1 +G1 X1 +G1 Y1 + +# Run bed_mesh_calibrate +BED_MESH_CALIBRATE + +G1 Z2 +BED_MESH_CALIBRATE METHOD=scan + +G1 Z2 +BED_MESH_CALIBRATE METHOD=rapid_scan + +# Move again +G1 Z5 X0 Y0 + +# Do regular probe +PROBE + +# Move again +G1 Z9 From bb963187259857211ca33bf798aef566b03c714b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 23:38:00 -0500 Subject: [PATCH 053/108] ci-install: Install scipy/numpy in github regression test case environment This is in preparation for enhanced load_cell test cases which require these packages. Signed-off-by: Kevin O'Connor --- scripts/ci-install.sh | 2 ++ scripts/tests-requirements.txt | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 scripts/tests-requirements.txt diff --git a/scripts/ci-install.sh b/scripts/ci-install.sh index 28f7b6540..88b346bc1 100755 --- a/scripts/ci-install.sh +++ b/scripts/ci-install.sh @@ -61,6 +61,7 @@ echo -e "\n\n=============== Install python3 virtualenv\n\n" cd ${MAIN_DIR} virtualenv -p python3 ${BUILD_DIR}/python-env ${BUILD_DIR}/python-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt +${BUILD_DIR}/python-env/bin/pip install -r ${MAIN_DIR}/scripts/tests-requirements.txt ###################################################################### @@ -71,3 +72,4 @@ echo -e "\n\n=============== Install python2 virtualenv\n\n" cd ${MAIN_DIR} virtualenv -p python2 ${BUILD_DIR}/python2-env ${BUILD_DIR}/python2-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt +${BUILD_DIR}/python2-env/bin/pip install -r ${MAIN_DIR}/scripts/tests-requirements.txt diff --git a/scripts/tests-requirements.txt b/scripts/tests-requirements.txt new file mode 100644 index 000000000..a3936f8ba --- /dev/null +++ b/scripts/tests-requirements.txt @@ -0,0 +1,7 @@ +# This file describes the Python virtualenv package requirements for +# the Klipper regression test cases. This is in addition to the +# package requirements listed in the klippy-requirements.txt file. +# Typically the packages listed here are installed via the command: +# pip install -r tests-requirements.txt +scipy==1.2.3 ; python_version < '3.0' +scipy==1.15.3 ; python_version >= '3.0' From 3c56eb7f6f9726ad46d10d404055a88118337ef5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 19 Jan 2026 23:27:24 -0500 Subject: [PATCH 054/108] load_cell_probe: Enhance regression test case Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell.py | 4 ++ klippy/extras/load_cell_probe.py | 2 + test/klippy/load_cell.cfg | 88 +++++++++++++++++++++++++++++--- test/klippy/load_cell.test | 21 +++++++- 4 files changed, 108 insertions(+), 7 deletions(-) diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py index f370cad90..361c937fc 100644 --- a/klippy/extras/load_cell.py +++ b/klippy/extras/load_cell.py @@ -313,6 +313,8 @@ class LoadCellSampleCollector: self._errors = 0 overflows = self._overflows self._overflows = 0 + if self._mcu.is_fileoutput(): + samples = [(0., 0., 0.)] return samples, (errors, overflows) if errors or overflows else 0 def _collect_until(self, timeout): @@ -324,6 +326,8 @@ class LoadCellSampleCollector: raise self._printer.command_error( "LoadCellSampleCollector timed out! Errors: %i," " Overflows: %i" % (self._errors, self._overflows)) + if self._mcu.is_fileoutput(): + break self._reactor.pause(now + RETRY_DELAY) return self._finish_collecting() diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index db2f2a650..1e08090c2 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -454,6 +454,8 @@ class LoadCellProbingMove: res = self._dispatch.stop() # clear the homing state so it stops processing samples self._last_trigger_time = self._mcu_load_cell_probe.clear_home() + if self._mcu.is_fileoutput(): + self._last_trigger_time = home_end_time if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: error = "Load Cell Probe Error: unknown reason code %i" % (res,) if res in self.ERROR_MAP: diff --git a/test/klippy/load_cell.cfg b/test/klippy/load_cell.cfg index fa599d10e..48ceca3ca 100644 --- a/test/klippy/load_cell.cfg +++ b/test/klippy/load_cell.cfg @@ -1,11 +1,75 @@ -# Test config for load_cell +# Test config for load cells +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: PK1 +position_endstop: 0.0 +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[bed_mesh] +mesh_min: 10,10 +mesh_max: 180,180 + [mcu] serial: /dev/ttyACM0 [printer] -kinematics: none +kinematics: cartesian max_velocity: 300 max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 [load_cell my_ads1220] sensor_type: ads1220 @@ -14,10 +78,22 @@ data_ready_pin: PA1 [load_cell my_hx711] sensor_type: hx711 -sclk_pin: PA2 -dout_pin: PA3 +sclk_pin: PA3 +dout_pin: PA5 [load_cell my_hx717] sensor_type: hx717 -sclk_pin: PA4 -dout_pin: PA5 +sclk_pin: PA7 +dout_pin: PJ0 + +[load_cell_probe] +z_offset: 0 +sensor_type: ads1220 +speed: 10.0 +cs_pin: PJ2 +data_ready_pin: PJ3 +counts_per_gram: 100 +reference_tare_counts: 1000 +drift_filter_cutoff_frequency: 0.8 +buzz_filter_cutoff_frequency: 100.0 +notch_filter_frequencies: 50, 60 diff --git a/test/klippy/load_cell.test b/test/klippy/load_cell.test index 880f840aa..c22de2be7 100644 --- a/test/klippy/load_cell.test +++ b/test/klippy/load_cell.test @@ -2,4 +2,23 @@ DICTIONARY atmega2560.dict CONFIG load_cell.cfg -G4 P1000 +# Start by homing the printer. +G28 +G1 F6000 + +# Z / X / Y moves +G1 Z1 +G1 X1 +G1 Y1 + +# Run bed_mesh_calibrate +BED_MESH_CALIBRATE + +# Move again +G1 Z5 X0 Y0 + +# Do regular probe +PROBE + +# Move again +G1 Z9 From 9ccb4d96e90f2ce89f061d17ce89760826260769 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 20 Dec 2025 16:41:44 -0500 Subject: [PATCH 055/108] manual_probe: Report final probe results in new ProbeResult named tuple Return the manual probe results in a named tuple containing (bed_x, bed_y, bed_z, test_x, test_y, and test_z) components. For a manual probe the test_xyz will always be equal to bed_xyz, but these components may differ when using automated z probes. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 6 ++--- klippy/extras/manual_probe.py | 30 ++++++++++++++++-------- klippy/extras/probe.py | 16 +++++++------ klippy/extras/probe_eddy_current.py | 6 ++--- klippy/extras/temperature_probe.py | 8 +++---- 5 files changed, 39 insertions(+), 27 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 908ac4dae..9f2e65d97 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -286,14 +286,14 @@ class Calibrater: # returns a callback function for the manual probe is_end = self.current_point_index == len(probe_points) - 1 - def callback(kin_pos): - if kin_pos is None: + def callback(mpresult): + if mpresult is None: # probe was cancelled self.gcmd.respond_info( "AXIS_TWIST_COMPENSATION_CALIBRATE: Probe cancelled, " "calibration aborted") return - z_offset = self.current_measured_z - kin_pos[2] + z_offset = self.current_measured_z - mpresult.bed_z self.results.append(z_offset) if is_end: # end of calibration diff --git a/klippy/extras/manual_probe.py b/klippy/extras/manual_probe.py index 44b2c719f..727526916 100644 --- a/klippy/extras/manual_probe.py +++ b/klippy/extras/manual_probe.py @@ -1,9 +1,18 @@ # Helper script for manual z height probing # -# Copyright (C) 2019 Kevin O'Connor +# Copyright (C) 2019-2025 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, bisect +import logging, bisect, collections + +# Main probe results tuple. The probe estimates that if the toollhead +# is commanded to xy position (bed_x, bed_y) and then descends, the +# nozzle will contact the bed at a toolhead z position of bed_z. The +# probe test itself was performed while the toolhead was at xyz +# position (test_x, test_y, test_z). All coordinates are relative to +# the frame (the coordinate system used in the config file). +ProbeResult = collections.namedtuple('probe_result', [ + 'bed_x', 'bed_y', 'bed_z', 'test_x', 'test_y', 'test_z']) # Helper to lookup the Z stepper config section def lookup_z_endstop_config(config): @@ -62,9 +71,9 @@ class ManualProbe: self.cmd_Z_OFFSET_APPLY_DELTA_ENDSTOPS, desc=self.cmd_Z_OFFSET_APPLY_ENDSTOP_help) self.reset_status() - def manual_probe_finalize(self, kin_pos): - if kin_pos is not None: - self.gcode.respond_info("Z position is %.3f" % (kin_pos[2],)) + def manual_probe_finalize(self, mpresult): + if mpresult is not None: + self.gcode.respond_info("Z position is %.3f" % (mpresult.bed_z,)) def reset_status(self): self.status = { 'is_active': False, @@ -77,10 +86,10 @@ class ManualProbe: cmd_MANUAL_PROBE_help = "Start manual probe helper script" def cmd_MANUAL_PROBE(self, gcmd): ManualProbeHelper(self.printer, gcmd, self.manual_probe_finalize) - def z_endstop_finalize(self, kin_pos): - if kin_pos is None: + def z_endstop_finalize(self, mpresult): + if mpresult is None: return - z_pos = self.z_position_endstop - kin_pos[2] + z_pos = self.z_position_endstop - mpresult.bed_z self.gcode.respond_info( "%s: position_endstop: %.3f\n" "The SAVE_CONFIG command will update the printer config file\n" @@ -271,10 +280,11 @@ class ManualProbeHelper: self.gcode.register_command('NEXT', None) self.gcode.register_command('ABORT', None) self.gcode.register_command('TESTZ', None) - kin_pos = None + mpresult = None if success: kin_pos = self.get_kinematics_pos() - self.finalize_callback(kin_pos) + mpresult = ProbeResult(*(kin_pos[:3] + kin_pos[:3])) + self.finalize_callback(mpresult) def load_config(config): return ManualProbe(config) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index d8f086265..15c3db255 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -83,10 +83,10 @@ class ProbeCommandHelper: pos = run_single_probe(self.probe, gcmd) gcmd.respond_info("Result is z=%.6f" % (pos[2],)) self.last_z_result = pos[2] - def probe_calibrate_finalize(self, kin_pos): - if kin_pos is None: + def probe_calibrate_finalize(self, mpresult): + if mpresult is None: return - z_offset = self.probe_calibrate_z - kin_pos[2] + z_offset = self.probe_calibrate_z - mpresult.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" @@ -514,7 +514,9 @@ class ProbePointsHelper: def _manual_probe_start(self): self._raise_tool(not self.manual_results) if len(self.manual_results) >= len(self.probe_points): - done = self._invoke_callback(self.manual_results) + results = [[mpr.bed_x, mpr.bed_y, mpr.bed_z] + for mpr in self.manual_results] + done = self._invoke_callback(results) if done: return # Caller wants a "retry" - clear results and restart probing @@ -523,10 +525,10 @@ class ProbePointsHelper: gcmd = self.gcode.create_gcode_command("", "", {}) manual_probe.ManualProbeHelper(self.printer, gcmd, self._manual_probe_finalize) - def _manual_probe_finalize(self, kin_pos): - if kin_pos is None: + def _manual_probe_finalize(self, mpresult): + if mpresult is None: return - self.manual_results.append(kin_pos) + self.manual_results.append(mpresult) self._manual_probe_start() # Helper to obtain a single probe measurement diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index b833f9ab9..876728352 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -206,11 +206,11 @@ class EddyCalibration: pos, mad_mm, mad_hz) gcode.respond_info(msg) return filtered - def post_manual_probe(self, kin_pos): - if kin_pos is None: + def post_manual_probe(self, mpresult): + if mpresult is None: # Manual Probe was aborted return - curpos = list(kin_pos) + curpos = [mpresult.bed_x, mpresult.bed_y, mpresult.bed_z] move = self.printer.lookup_object('toolhead').manual_move # Move away from the bed probe_calibrate_z = curpos[2] diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index aebb10764..d42d9e210 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -221,19 +221,19 @@ class TemperatureProbe: % (self.name, cnt, exp_cnt, last_temp, self.next_auto_temp) ) - def _manual_probe_finalize(self, kin_pos): - if kin_pos is None: + def _manual_probe_finalize(self, mpresult): + if mpresult is None: # Calibration aborted self._finalize_drift_cal(False) return if self.last_zero_pos is not None: - z_diff = self.last_zero_pos[2] - kin_pos[2] + z_diff = self.last_zero_pos - mpresult.bed_z self.total_expansion += z_diff logging.info( "Estimated Total Thermal Expansion: %.6f" % (self.total_expansion,) ) - self.last_zero_pos = kin_pos + self.last_zero_pos = mpresult.bed_z toolhead = self.printer.lookup_object("toolhead") tool_zero_z = toolhead.get_position()[2] try: From 2e0c2262e7c52cbc2ea4784e582fac2f1d3b9d67 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 21 Dec 2025 12:28:36 -0500 Subject: [PATCH 056/108] probe: Convert ProbePointsHelper to use ProbeResult Change the ProbePointsHelper class to return ProbeResult tuples. Callers of this class are also updated so that they use the tuple's bed_xyz parameters instead of manually calculating these values from the probe xyz offsets. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 14 +++++++++++--- klippy/extras/bed_mesh.py | 22 +++++++++++++--------- klippy/extras/bed_tilt.py | 13 +++++-------- klippy/extras/delta_calibrate.py | 8 ++++---- klippy/extras/probe.py | 10 ++++++---- klippy/extras/quad_gantry_level.py | 20 ++++++++++---------- klippy/extras/screws_tilt_adjust.py | 8 ++++---- klippy/extras/z_tilt.py | 13 +++++-------- 8 files changed, 58 insertions(+), 50 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 6846e034d..76f455af5 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,9 +8,17 @@ All dates in this document are approximate. ## Changes -20251122: An option `axis` has been added to `[carriage ]` sections -for `generic_cartesian` kinematics, allowing arbitrary names for primary -carriages. Users are encouraged to explicitly specify `axis` option now. +20260109: The `[screws_tilt_adjust]` module now reports the status +variable `{printer.screws_tilt_adjust.result.screw1.z}` with the +probe's `z_offset` applied. That is, one would previously need to +subtract the probe's configured `z_offset` to find the absolute Z +deviation at the given screw location and now one must not apply the +`z_offset`. + +20251122: An option `axis` has been added to `[carriage ]` +sections for `generic_cartesian` kinematics, allowing arbitrary names +for primary carriages. Users are encouraged to explicitly specify +`axis` option now. 20251106: The status fields `{printer.toolhead.position}`, `{printer.gcode_move.position}`, diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index a8e5764c0..f752980e0 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -651,9 +651,9 @@ class BedMeshCalibrate: except BedMeshError as e: raise gcmd.error(str(e)) self.probe_mgr.start_probe(gcmd) - def probe_finalize(self, offsets, positions): - z_offset = offsets[2] - positions = [[round(p[0], 2), round(p[1], 2), p[2]] + def probe_finalize(self, positions): + z_offset = 0. + positions = [[round(p.bed_x, 2), round(p.bed_y, 2), p.bed_z] for p in positions] if self.probe_mgr.get_zero_ref_mode() == ZrefMode.PROBE: ref_pos = positions.pop() @@ -682,7 +682,7 @@ class BedMeshCalibrate: idx_offset = 0 start_idx = 0 for i, pts in substitutes.items(): - fpt = [p - o for p, o in zip(base_points[i], offsets[:2])] + fpt = base_points[i][:2] # offset the index to account for additional samples idx = i + idx_offset # Add "normal" points @@ -702,7 +702,7 @@ class BedMeshCalibrate: # validate length of result if len(base_points) != len(positions): - self._dump_points(probed_pts, positions, offsets) + self._dump_points(probed_pts, positions) raise self.gcode.error( "bed_mesh: invalid position list size, " "generated count: %d, probed count: %d" @@ -713,7 +713,7 @@ class BedMeshCalibrate: row = [] prev_pos = base_points[0] for pos, result in zip(base_points, positions): - offset_pos = [p - o for p, o in zip(pos, offsets[:2])] + offset_pos = pos[:2] if ( not isclose(offset_pos[0], result[0], abs_tol=.5) or not isclose(offset_pos[1], result[1], abs_tol=.5) @@ -786,7 +786,7 @@ class BedMeshCalibrate: self.gcode.respond_info("Mesh Bed Leveling Complete") if self._profile_name is not None: self.bedmesh.save_profile(self._profile_name) - def _dump_points(self, probed_pts, corrected_pts, offsets): + def _dump_points(self, probed_pts, corrected_pts): # logs generated points with offset applied, points received # from the finalize callback, and the list of corrected points points = self.probe_mgr.get_base_points() @@ -797,7 +797,7 @@ class BedMeshCalibrate: for i in list(range(max_len)): gen_pt = probed_pt = corr_pt = "" if i < len(points): - off_pt = [p - o for p, o in zip(points[i], offsets[:2])] + off_pt = points[i][:2] gen_pt = "(%.2f, %.2f)" % tuple(off_pt) if i < len(probed_pts): probed_pt = "(%.2f, %.2f, %.4f)" % tuple(probed_pts[i]) @@ -1220,8 +1220,12 @@ class RapidScanHelper: if is_probe_pt: probe_session.run_probe(gcmd) results = probe_session.pull_probed_results() + import manual_probe # XXX + results = [manual_probe.ProbeResult( + r[0]+offsets[0], r[1]+offsets[1], r[2]-offsets[2], r[0], r[1], r[2]) + for r in results] toolhead.get_last_move_time() - self.finalize_callback(offsets, results) + self.finalize_callback(results) probe_session.end_probe_session() def _raise_tool(self, gcmd, scan_height): diff --git a/klippy/extras/bed_tilt.py b/klippy/extras/bed_tilt.py index e5686cbeb..feb924997 100644 --- a/klippy/extras/bed_tilt.py +++ b/klippy/extras/bed_tilt.py @@ -58,19 +58,17 @@ class BedTiltCalibrate: cmd_BED_TILT_CALIBRATE_help = "Bed tilt calibration script" def cmd_BED_TILT_CALIBRATE(self, gcmd): self.probe_helper.start_probe(gcmd) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Setup for coordinate descent analysis - z_offset = offsets[2] logging.info("Calculating bed_tilt with: %s", positions) params = { 'x_adjust': self.bedtilt.x_adjust, 'y_adjust': self.bedtilt.y_adjust, - 'z_adjust': z_offset } + 'z_adjust': 0. } logging.info("Initial bed_tilt parameters: %s", params) # Perform coordinate descent def adjusted_height(pos, params): - x, y, z = pos - return (z - x*params['x_adjust'] - y*params['y_adjust'] - - params['z_adjust']) + return (pos.bed_z - pos.bed_x*params['x_adjust'] + - pos.bed_y*params['y_adjust'] - params['z_adjust']) def errorfunc(params): total_error = 0. for pos in positions: @@ -81,8 +79,7 @@ class BedTiltCalibrate: # Update current bed_tilt calculations x_adjust = new_params['x_adjust'] y_adjust = new_params['y_adjust'] - z_adjust = (new_params['z_adjust'] - z_offset - - x_adjust * offsets[0] - y_adjust * offsets[1]) + z_adjust = new_params['z_adjust'] self.bedtilt.update_adjust(x_adjust, y_adjust, z_adjust) # Log and report results logging.info("Calculated bed_tilt parameters: %s", new_params) diff --git a/klippy/extras/delta_calibrate.py b/klippy/extras/delta_calibrate.py index 4054e2310..df7f39356 100644 --- a/klippy/extras/delta_calibrate.py +++ b/klippy/extras/delta_calibrate.py @@ -152,12 +152,12 @@ class DeltaCalibrate: "%.3f,%.3f,%.3f" % tuple(spos1)) configfile.set(section, "distance%d_pos2" % (i,), "%.3f,%.3f,%.3f" % tuple(spos2)) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Convert positions into (z_offset, stable_position) pairs - z_offset = offsets[2] kin = self.printer.lookup_object('toolhead').get_kinematics() - delta_params = kin.get_calibration() - probe_positions = [(z_offset, delta_params.calc_stable_position(p)) + csp = kin.get_calibration().calc_stable_position + probe_positions = [(p.test_z - p.bed_z, + csp([p.test_x, p.test_y, p.test_z])) for p in positions] # Perform analysis self.calculate_params(probe_positions, self.last_distances) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 15c3db255..600d64e47 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -466,7 +466,7 @@ class ProbePointsHelper: toolhead = self.printer.lookup_object('toolhead') toolhead.get_last_move_time() # Invoke callback - res = self.finalize_callback(self.probe_offsets, results) + res = self.finalize_callback(results) return res != "retry" def _move_next(self, probe_num): # Move to next XY probe point @@ -502,6 +502,10 @@ class ProbePointsHelper: self._raise_tool(not probe_num) if probe_num >= len(self.probe_points): results = probe_session.pull_probed_results() + results = [manual_probe.ProbeResult( + r[0] + self.probe_offsets[0], r[1] + self.probe_offsets[1], + r[2] - self.probe_offsets[2], r[0], r[1], r[2]) + for r in results] done = self._invoke_callback(results) if done: break @@ -514,9 +518,7 @@ class ProbePointsHelper: def _manual_probe_start(self): self._raise_tool(not self.manual_results) if len(self.manual_results) >= len(self.probe_points): - results = [[mpr.bed_x, mpr.bed_y, mpr.bed_z] - for mpr in self.manual_results] - done = self._invoke_callback(results) + done = self._invoke_callback(self.manual_results) if done: return # Caller wants a "retry" - clear results and restart probing diff --git a/klippy/extras/quad_gantry_level.py b/klippy/extras/quad_gantry_level.py index 98cd53c5a..5556d5e1d 100644 --- a/klippy/extras/quad_gantry_level.py +++ b/klippy/extras/quad_gantry_level.py @@ -51,34 +51,34 @@ class QuadGantryLevel: self.z_status.reset() self.retry_helper.start(gcmd) self.probe_helper.start_probe(gcmd) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Mirror our perspective so the adjustments make sense # from the perspective of the gantry - z_positions = [self.horizontal_move_z - p[2] for p in positions] + z_positions = [self.horizontal_move_z - p.bed_z for p in positions] points_message = "Gantry-relative probe points:\n%s\n" % ( " ".join(["%s: %.6f" % (z_id, z_positions[z_id]) for z_id in range(len(z_positions))])) self.gcode.respond_info(points_message) # Calculate slope along X axis between probe point 0 and 3 - ppx0 = [positions[0][0] + offsets[0], z_positions[0]] - ppx3 = [positions[3][0] + offsets[0], z_positions[3]] + ppx0 = [positions[0].bed_x, z_positions[0]] + ppx3 = [positions[3].bed_x, z_positions[3]] slope_x_pp03 = self.linefit(ppx0, ppx3) # Calculate slope along X axis between probe point 1 and 2 - ppx1 = [positions[1][0] + offsets[0], z_positions[1]] - ppx2 = [positions[2][0] + offsets[0], z_positions[2]] + ppx1 = [positions[1].bed_x, z_positions[1]] + ppx2 = [positions[2].bed_x, z_positions[2]] slope_x_pp12 = self.linefit(ppx1, ppx2) logging.info("quad_gantry_level f1: %s, f2: %s" % (slope_x_pp03, slope_x_pp12)) # Calculate gantry slope along Y axis between stepper 0 and 1 - a1 = [positions[0][1] + offsets[1], + a1 = [positions[0].bed_y, self.plot(slope_x_pp03, self.gantry_corners[0][0])] - a2 = [positions[1][1] + offsets[1], + a2 = [positions[1].bed_y, self.plot(slope_x_pp12, self.gantry_corners[0][0])] slope_y_s01 = self.linefit(a1, a2) # Calculate gantry slope along Y axis between stepper 2 and 3 - b1 = [positions[0][1] + offsets[1], + b1 = [positions[0].bed_y, self.plot(slope_x_pp03, self.gantry_corners[1][0])] - b2 = [positions[1][1] + offsets[1], + b2 = [positions[1].bed_y, self.plot(slope_x_pp12, self.gantry_corners[1][0])] slope_y_s23 = self.linefit(b1, b2) logging.info("quad_gantry_level af: %s, bf: %s" diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index b988c7cef..b3327ba49 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -63,7 +63,7 @@ class ScrewsTiltAdjust: 'max_deviation': self.max_diff, 'results': self.results} - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): self.results = {} self.max_diff_error = False # Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6 @@ -79,15 +79,15 @@ class ScrewsTiltAdjust: or (not is_clockwise_thread and self.direction == 'CCW')) min_or_max = max if use_max else min i_base, z_base = min_or_max( - enumerate([pos[2] for pos in positions]), key=lambda v: v[1]) + enumerate([pos.bed_z for pos in positions]), key=lambda v: v[1]) else: # First screw is the base position used for comparison - i_base, z_base = 0, positions[0][2] + i_base, z_base = 0, positions[0].bed_z # Provide the user some information on how to read the results self.gcode.respond_info("01:20 means 1 full turn and 20 minutes, " "CW=clockwise, CCW=counter-clockwise") for i, screw in enumerate(self.screws): - z = positions[i][2] + z = positions[i].bed_z coord, name = screw if i == i_base: # Show the results diff --git a/klippy/extras/z_tilt.py b/klippy/extras/z_tilt.py index 0316ee721..28763f1f0 100644 --- a/klippy/extras/z_tilt.py +++ b/klippy/extras/z_tilt.py @@ -143,16 +143,14 @@ class ZTilt: self.z_status.reset() self.retry_helper.start(gcmd) self.probe_helper.start_probe(gcmd) - def probe_finalize(self, offsets, positions): + def probe_finalize(self, positions): # Setup for coordinate descent analysis - z_offset = offsets[2] logging.info("Calculating bed tilt with: %s", positions) - params = { 'x_adjust': 0., 'y_adjust': 0., 'z_adjust': z_offset } + params = { 'x_adjust': 0., 'y_adjust': 0., 'z_adjust': 0. } # Perform coordinate descent def adjusted_height(pos, params): - x, y, z = pos - return (z - x*params['x_adjust'] - y*params['y_adjust'] - - params['z_adjust']) + return (pos.bed_z - pos.bed_x*params['x_adjust'] + - pos.bed_y*params['y_adjust'] - params['z_adjust']) def errorfunc(params): total_error = 0. for pos in positions: @@ -165,8 +163,7 @@ class ZTilt: logging.info("Calculated bed tilt parameters: %s", new_params) x_adjust = new_params['x_adjust'] y_adjust = new_params['y_adjust'] - z_adjust = (new_params['z_adjust'] - z_offset - - x_adjust * offsets[0] - y_adjust * offsets[1]) + z_adjust = new_params['z_adjust'] adjustments = [x*x_adjust + y*y_adjust + z_adjust for x, y in self.z_positions] self.z_helper.adjust_steppers(adjustments, speed) From 252fc18c121b7f17b6070767eeccc6d99f1b740c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 11:00:16 -0500 Subject: [PATCH 057/108] probe: Pass probe_offsets to HomingViaProbeHelper() class Signed-off-by: Kevin O'Connor --- klippy/extras/bltouch.py | 4 ++-- klippy/extras/probe.py | 7 ++++--- klippy/extras/probe_eddy_current.py | 3 ++- klippy/extras/smart_effector.py | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index 2bcb9cc10..e39f70dbb 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -65,8 +65,8 @@ class BLTouchProbe: config, self, self.mcu_endstop.query_endstop) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) - self.homing_helper = probe.HomingViaProbeHelper(config, self, - self.param_helper) + self.homing_helper = probe.HomingViaProbeHelper( + config, self, self.probe_offsets, self.param_helper) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.homing_helper.start_probe_session) # Register BLTOUCH_DEBUG command diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 600d64e47..d09337223 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -198,9 +198,10 @@ class LookupZSteppers: # Homing via probe:z_virtual_endstop class HomingViaProbeHelper: - def __init__(self, config, mcu_probe, param_helper): + def __init__(self, config, mcu_probe, probe_offsets, param_helper): self.printer = config.get_printer() self.mcu_probe = mcu_probe + self.probe_offsets = probe_offsets self.param_helper = param_helper self.multi_probe_pending = False self.z_min_position = lookup_minimum_z(config) @@ -613,8 +614,8 @@ class PrinterProbe: self.mcu_probe.query_endstop) self.probe_offsets = ProbeOffsetsHelper(config) self.param_helper = ProbeParameterHelper(config) - self.homing_helper = HomingViaProbeHelper(config, self.mcu_probe, - self.param_helper) + self.homing_helper = HomingViaProbeHelper( + config, self.mcu_probe, self.probe_offsets, self.param_helper) self.probe_session = ProbeSessionHelper( config, self.param_helper, self.homing_helper.start_probe_session) def get_probe_params(self, gcmd=None): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 876728352..3e10f2a0f 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -549,7 +549,8 @@ class PrinterEddyProbe: self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) - probe.HomingViaProbeHelper(config, mcu_probe, self.param_helper) + probe.HomingViaProbeHelper( + config, mcu_probe, self.probe_offsets, self.param_helper) self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 3caa4e6b7..87369f4f3 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -70,8 +70,8 @@ class SmartEffectorProbe: config, self, self.probe_wrapper.query_endstop) self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) - self.homing_helper = probe.HomingViaProbeHelper(config, self, - self.param_helper) + self.homing_helper = probe.HomingViaProbeHelper( + config, self, self.probe_offsets, self.param_helper) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.homing_helper.start_probe_session) # SmartEffector control From 8c2c90b8d6aeea18bb062a919d704b944f65fb4e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 11:20:26 -0500 Subject: [PATCH 058/108] probe_eddy_current: Pass probe_offsets class to EddyDescend Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 3e10f2a0f..a8d68dbdb 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -278,11 +278,11 @@ class EddyCalibration: # Tool to gather samples and convert them to probe positions class EddyGatherSamples: - def __init__(self, printer, sensor_helper, calibration, z_offset): + def __init__(self, printer, sensor_helper, calibration, offsets): self._printer = printer self._sensor_helper = sensor_helper self._calibration = calibration - self._z_offset = z_offset + self._offsets = offsets # Results storage self._samples = [] self._probe_times = [] @@ -375,8 +375,9 @@ class EddyGatherSamples: raise self._printer.command_error( "probe_eddy_current sensor not in valid range") # Callers expect position relative to z_offset, so recalculate + z_offset = self._offsets[2] bed_deviation = toolhead_pos[2] - sensor_z - toolhead_pos[2] = self._z_offset + bed_deviation + toolhead_pos[2] = z_offset + bed_deviation results.append(toolhead_pos) del self._probe_results[:] return results @@ -390,14 +391,15 @@ class EddyGatherSamples: # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - def __init__(self, config, sensor_helper, calibration, param_helper): + def __init__(self, config, sensor_helper, calibration, + probe_offsets, param_helper): self._printer = config.get_printer() self._sensor_helper = sensor_helper self._mcu = sensor_helper.get_mcu() self._calibration = calibration + self._probe_offsets = probe_offsets self._param_helper = param_helper self._z_min_position = probe.lookup_minimum_z(config) - self._z_offset = config.getfloat('z_offset', minval=0.) self._dispatch = mcu.TriggerDispatch(self._mcu) self._trigger_time = 0. self._gather = None @@ -408,7 +410,8 @@ class EddyDescend: def home_start(self, print_time, sample_time, sample_count, rest_time, triggered=True): self._trigger_time = 0. - trigger_freq = self._calibration.height_to_freq(self._z_offset) + z_offset = self._probe_offsets.get_offsets()[2] + trigger_freq = self._calibration.height_to_freq(z_offset) trigger_completion = self._dispatch.start(print_time) self._sensor_helper.setup_home( print_time, trigger_freq, self._dispatch.get_oid(), @@ -433,8 +436,9 @@ class EddyDescend: return trigger_time # Probe session interface def start_probe_session(self, gcmd): + offsets = self._probe_offsets.get_offsets() self._gather = EddyGatherSamples(self._printer, self._sensor_helper, - self._calibration, self._z_offset) + self._calibration, offsets) return self def run_probe(self, gcmd): toolhead = self._printer.lookup_object('toolhead') @@ -488,17 +492,19 @@ class EddyEndstopWrapper: def probe_finish(self, hmove): pass def get_position_endstop(self): - return self._eddy_descend._z_offset + z_offset = self._eddy_descend._probe_offsets.get_offsets()[2] + return z_offset # Implementing probing with "METHOD=scan" class EddyScanningProbe: - def __init__(self, printer, sensor_helper, calibration, z_offset, gcmd): + def __init__(self, printer, sensor_helper, calibration, probe_offsets, + gcmd): self._printer = printer self._sensor_helper = sensor_helper self._calibration = calibration - self._z_offset = z_offset + offsets = probe_offsets.get_offsets() self._gather = EddyGatherSamples(printer, sensor_helper, - calibration, z_offset) + calibration, offsets) self._sample_time_delay = 0.050 self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' @@ -540,12 +546,13 @@ class PrinterEddyProbe: sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) # Probe interface + self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( - config, self.sensor_helper, self.calibration, self.param_helper) + config, self.sensor_helper, self.calibration, self.probe_offsets, + self.param_helper) self.cmd_helper = probe.ProbeCommandHelper(config, self, replace_z_offset=True) - self.probe_offsets = probe.ProbeOffsetsHelper(config) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) @@ -563,9 +570,8 @@ class PrinterEddyProbe: def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): - z_offset = self.get_offsets()[2] return EddyScanningProbe(self.printer, self.sensor_helper, - self.calibration, z_offset, gcmd) + self.calibration, self.probe_offsets, gcmd) return self.probe_session.start_probe_session(gcmd) def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) From f33c76da2274b745f1051de09a52f484b0bff8ed Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 23 Dec 2025 11:25:56 -0500 Subject: [PATCH 059/108] load_cell_probe: Pass probe_offsets to TapSession() Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 1e08090c2..df12c9fc3 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -544,9 +544,11 @@ class TappingMove: # ProbeSession that implements Tap logic class TapSession: - def __init__(self, config, tapping_move, probe_params_helper): + def __init__(self, config, tapping_move, + probe_offsets, probe_params_helper): self._printer = config.get_printer() self._tapping_move = tapping_move + self._probe_offsets = probe_offsets self._probe_params_helper = probe_params_helper # Session state self._results = [] @@ -632,7 +634,8 @@ class LoadCellPrinterProbe: continuous_tare_filter_helper, config_helper) self._tapping_move = TappingMove(config, load_cell_probing_move, config_helper) - tap_session = TapSession(config, self._tapping_move, self._param_helper) + tap_session = TapSession(config, self._tapping_move, + self._probe_offsets, self._param_helper) self._probe_session = probe.ProbeSessionHelper(config, self._param_helper, tap_session.start_probe_session) # printer integration From 2a1027ce4104f4f10ce17a143514fc3fcad9a6ab Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 22 Dec 2025 22:17:39 -0500 Subject: [PATCH 060/108] probe: Convert pull_probed_results() to return ProbeResult Change the low-level probe code to return ProbeResult tuples from probe_session.pull_probed_results(). Also update callers to use the calculated bed_xyz values found in the tuple instead of calculating them from the probe's xyz offsets. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 7 +++ klippy/extras/axis_twist_compensation.py | 18 ++++--- klippy/extras/bed_mesh.py | 4 -- klippy/extras/load_cell_probe.py | 3 +- klippy/extras/probe.py | 65 +++++++++++++----------- klippy/extras/probe_eddy_current.py | 12 ++--- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 76f455af5..384908727 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,13 @@ All dates in this document are approximate. ## Changes +20260109: The g-code console text output from the `PROBE`, +`PROBE_ACCURACY`, and similar commands has changed. Now Z heights are +reported relative to the nominal bed Z position instead of relative to +the probe's configured `z_offset`. Similarly, intermediate probe x and +y console reports will also have the probe's configured `x_offset` and +`y_offset` applied. + 20260109: The `[screws_tilt_adjust]` module now reports the status variable `{printer.screws_tilt_adjust.result.screw1.z}` with the probe's `z_offset` applied. That is, one would previously need to diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 9f2e65d97..28f01d2d6 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -51,21 +51,27 @@ class AxisTwistCompensation: self.printer.register_event_handler("probe:update_results", self._update_z_compensation_value) - def _update_z_compensation_value(self, pos): + def _update_z_compensation_value(self, poslist): + pos = poslist[0] + zo = 0. if self.z_compensations: - pos[2] += self._get_interpolated_z_compensation( - pos[0], self.z_compensations, + zo += self._get_interpolated_z_compensation( + pos.test_x, self.z_compensations, self.compensation_start_x, self.compensation_end_x ) if self.zy_compensations: - pos[2] += self._get_interpolated_z_compensation( - pos[1], self.zy_compensations, + zo += self._get_interpolated_z_compensation( + pos.test_y, self.zy_compensations, self.compensation_start_y, self.compensation_end_y ) + pos = manual_probe.ProbeResult(pos.bed_x, pos.bed_y, pos.bed_z + zo, + pos.test_x, pos.test_y, pos.test_z) + poslist[0] = pos + def _get_interpolated_z_compensation( self, coord, z_compensations, comp_start, @@ -267,7 +273,7 @@ class Calibrater: # probe the point pos = probe.run_single_probe(self.probe, self.gcmd) - self.current_measured_z = pos[2] + self.current_measured_z = pos.bed_z # horizontal_move_z (to prevent probe trigger or hitting bed) self._move_helper((None, None, self.horizontal_move_z)) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index f752980e0..822d43efd 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -1220,10 +1220,6 @@ class RapidScanHelper: if is_probe_pt: probe_session.run_probe(gcmd) results = probe_session.pull_probed_results() - import manual_probe # XXX - results = [manual_probe.ProbeResult( - r[0]+offsets[0], r[1]+offsets[1], r[2]-offsets[2], r[0], r[1], r[2]) - for r in results] toolhead.get_last_move_time() self.finalize_callback(results) probe_session.end_probe_session() diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index df12c9fc3..1bfe738fa 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -562,7 +562,8 @@ class TapSession: # probe until a single good sample is returned or retries are exhausted def run_probe(self, gcmd): epos, is_good = self._tapping_move.run_tap(gcmd) - self._results.append(epos) + res = self._probe_offsets.create_probe_result(epos) + self._results.append(res) def pull_probed_results(self): res = self._results diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index d09337223..fcf8cc697 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -17,11 +17,12 @@ can travel further (the Z minimum position can be negative). def calc_probe_z_average(positions, method='average'): if method != 'median': # Use mean average - count = float(len(positions)) - return [sum([pos[i] for pos in positions]) / count - for i in range(3)] + inv_count = 1. / float(len(positions)) + return manual_probe.ProbeResult( + *[sum([pos[i] for pos in positions]) * inv_count + for i in range(len(positions[0]))]) # Use median - z_sorted = sorted(positions, key=(lambda p: p[2])) + z_sorted = sorted(positions, key=(lambda p: p.bed_z)) middle = len(positions) // 2 if (len(positions) & 1) == 1: # odd number of samples @@ -52,7 +53,7 @@ class ProbeCommandHelper: gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) # PROBE_CALIBRATE command - self.probe_calibrate_z = 0. + self.probe_calibrate_pos = None gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, desc=self.cmd_PROBE_CALIBRATE_help) # Other commands @@ -81,12 +82,15 @@ class ProbeCommandHelper: cmd_PROBE_help = "Probe Z-height at current XY position" def cmd_PROBE(self, gcmd): pos = run_single_probe(self.probe, gcmd) - gcmd.respond_info("Result is z=%.6f" % (pos[2],)) - self.last_z_result = pos[2] + gcmd.respond_info("Result: at %.3f,%.3f estimate contact at z=%.6f" + % (pos.bed_x, pos.bed_y, pos.bed_z)) + x_offset, y_offset, z_offset = self.probe.get_offsets() + self.last_z_result = pos.bed_z + z_offset # XXX def probe_calibrate_finalize(self, mpresult): if mpresult is None: return - z_offset = self.probe_calibrate_z - mpresult.bed_z + x_offset, y_offset, z_offset = self.probe.get_offsets() + z_offset += mpresult.bed_z - self.probe_calibrate_pos.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" @@ -99,17 +103,17 @@ class ProbeCommandHelper: manual_probe.verify_no_manual_probe(self.printer) params = self.probe.get_probe_params(gcmd) # Perform initial probe - curpos = run_single_probe(self.probe, gcmd) + pos = run_single_probe(self.probe, gcmd) # Move away from the bed - self.probe_calibrate_z = curpos[2] + curpos = self.printer.lookup_object('toolhead').get_position() curpos[2] += 5. self._move(curpos, params['lift_speed']) # Move the nozzle over the probe point - x_offset, y_offset, z_offset = self.probe.get_offsets() - curpos[0] += x_offset - curpos[1] += y_offset + curpos[0] = pos.bed_x + curpos[1] = pos.bed_y self._move(curpos, params['probe_speed']) # Start manual probe + self.probe_calibrate_pos = pos manual_probe.ManualProbeHelper(self.printer, gcmd, self.probe_calibrate_finalize) cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" @@ -143,15 +147,15 @@ class ProbeCommandHelper: positions = probe_session.pull_probed_results() probe_session.end_probe_session() # Calculate maximum, minimum and average values - max_value = max([p[2] for p in positions]) - min_value = min([p[2] for p in positions]) + max_value = max([p.bed_z for p in positions]) + min_value = min([p.bed_z for p in positions]) range_value = max_value - min_value - avg_value = calc_probe_z_average(positions, 'average')[2] - median = calc_probe_z_average(positions, 'median')[2] + avg_value = calc_probe_z_average(positions, 'average').bed_z + median = calc_probe_z_average(positions, 'median').bed_z # calculate the standard deviation deviation_sum = 0 for i in range(len(positions)): - deviation_sum += pow(positions[i][2] - avg_value, 2.) + deviation_sum += pow(positions[i].bed_z - avg_value, 2.) sigma = (deviation_sum / len(positions)) ** 0.5 # Show information gcmd.respond_info( @@ -260,7 +264,9 @@ class HomingViaProbeHelper: pos[2] = self.z_min_position speed = self.param_helper.get_probe_params(gcmd)['probe_speed'] phoming = self.printer.lookup_object('homing') - self.results.append(phoming.probing_move(self.mcu_probe, pos, speed)) + ppos = phoming.probing_move(self.mcu_probe, pos, speed) + res = self.probe_offsets.create_probe_result(ppos) + self.results.append(res) def pull_probed_results(self): res = self.results self.results = [] @@ -368,12 +374,12 @@ class ProbeSessionHelper: reason += HINT_TIMEOUT raise self.printer.command_error(reason) # Allow axis_twist_compensation to update results - self.printer.send_event("probe:update_results", epos) + self.printer.send_event("probe:update_results", [epos]) # Report results gcode = self.printer.lookup_object('gcode') - gcode.respond_info("probe at %.3f,%.3f is z=%.6f" - % (epos[0], epos[1], epos[2])) - return epos[:3] + gcode.respond_info("probe: at %.3f,%.3f bed will contact at z=%.6f" + % (epos.bed_x, epos.bed_y, epos.bed_z)) + return epos def run_probe(self, gcmd): if self.hw_probe_session is None: self._probe_state_error() @@ -388,7 +394,7 @@ class ProbeSessionHelper: pos = self._probe(gcmd) positions.append(pos) # Check samples tolerance - z_positions = [p[2] for p in positions] + z_positions = [p.bed_z for p in positions] if max(z_positions)-min(z_positions) > params['samples_tolerance']: if retries >= params['samples_tolerance_retries']: raise gcmd.error("Probe samples exceed samples_tolerance") @@ -397,8 +403,9 @@ class ProbeSessionHelper: positions = [] # Retract if len(positions) < sample_count: + cur_z = toolhead.get_position()[2] toolhead.manual_move( - probexy + [pos[2] + params['sample_retract_dist']], + probexy + [cur_z + params['sample_retract_dist']], params['lift_speed']) # Calculate result epos = calc_probe_z_average(positions, params['samples_result']) @@ -416,6 +423,10 @@ class ProbeOffsetsHelper: self.z_offset = config.getfloat('z_offset') def get_offsets(self): return self.x_offset, self.y_offset, self.z_offset + def create_probe_result(self, test_pos): + return manual_probe.ProbeResult( + test_pos[0]+self.x_offset, test_pos[1]+self.y_offset, + test_pos[2]-self.z_offset, test_pos[0], test_pos[1], test_pos[2]) ###################################################################### @@ -503,10 +514,6 @@ class ProbePointsHelper: self._raise_tool(not probe_num) if probe_num >= len(self.probe_points): results = probe_session.pull_probed_results() - results = [manual_probe.ProbeResult( - r[0] + self.probe_offsets[0], r[1] + self.probe_offsets[1], - r[2] - self.probe_offsets[2], r[0], r[1], r[2]) - for r in results] done = self._invoke_callback(results) if done: break diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index a8d68dbdb..161b675b0 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -374,11 +374,11 @@ class EddyGatherSamples: if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: raise self._printer.command_error( "probe_eddy_current sensor not in valid range") - # Callers expect position relative to z_offset, so recalculate - z_offset = self._offsets[2] - bed_deviation = toolhead_pos[2] - sensor_z - toolhead_pos[2] = z_offset + bed_deviation - results.append(toolhead_pos) + res = manual_probe.ProbeResult( + toolhead_pos[0]+self._offsets[0], + toolhead_pos[1]+self._offsets[1], toolhead_pos[2]-sensor_z, + toolhead_pos[0], toolhead_pos[1], toolhead_pos[2]) + results.append(res) del self._probe_results[:] return results def note_probe(self, start_time, end_time, toolhead_pos): @@ -530,7 +530,7 @@ class EddyScanningProbe: results = self._gather.pull_probed() # Allow axis_twist_compensation to update results for epos in results: - self._printer.send_event("probe:update_results", epos) + self._printer.send_event("probe:update_results", [epos]) return results def end_probe_session(self): self._gather.finish() From 32a5f2b042ba32b071892b3268d06e6756b0c15d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Dec 2025 21:28:41 -0500 Subject: [PATCH 061/108] probe: Deprecate last_z_result and add new last_probe_position Deprecate the PROBE command's exported value `{printer.probe.last_z_result}`. This value effectively returns the toolhead Z position when the probe triggers and user's then need to adjust the result using the probe's configured z_offset. Introduce a new `{printer.probe.last_probe_position}` as a replacement. This replacement has an easier to understand behavior - it states that the probe hardware estimates that if the toolhead is commanded to last_probe_position.x, last_probe_position.y and descends then the tip of the toolhead should first make contact at a Z height of last_probe_position.z . That is, the new exported value already takes into account the probe's configured xyz offsets. Signed-off-by: Kevin O'Connor --- docs/Config_Changes.md | 5 +++++ docs/Load_Cell.md | 4 ++-- docs/Status_Reference.md | 16 ++++++++++++---- klippy/extras/probe.py | 7 ++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 384908727..cf2d53fd9 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,11 @@ All dates in this document are approximate. ## Changes +20260109: The status value `{printer.probe.last_z_result}` is +deprecated; it will be removed in the near future. Use +`{printer.probe.last_probe_position}` instead, and note that this new +value already has the probe's configured xyz offsets applied. + 20260109: The g-code console text output from the `PROBE`, `PROBE_ACCURACY`, and similar commands has changed. Now Z heights are reported relative to the nominal bed Z position instead of relative to diff --git a/docs/Load_Cell.md b/docs/Load_Cell.md index 8345cae49..c6cf9ce49 100644 --- a/docs/Load_Cell.md +++ b/docs/Load_Cell.md @@ -252,12 +252,12 @@ macro. This requires setting up Here is a simple macro that can accomplish this. Note that the `_HOME_Z_FROM_LAST_PROBE` macro has to be separate because of the way macros work. The sub-call is needed so that the `_HOME_Z_FROM_LAST_PROBE` macro can -see the result of the probe in `printer.probe.last_z_result`. +see the result of the probe in `printer.probe.last_probe_position`. ```gcode [gcode_macro _HOME_Z_FROM_LAST_PROBE] gcode: - {% set z_probed = printer.probe.last_z_result %} + {% set z_probed = printer.probe.last_probe_position.z %} {% set z_position = printer.toolhead.position[2] %} {% set z_actual = z_position - z_probed %} SET_KINEMATIC_POSITION Z={z_actual} diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 4e4ed5c76..1f6704acc 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -419,10 +419,18 @@ is defined): during the last QUERY_PROBE command. Note, if this is used in a macro, due to the order of template expansion, the QUERY_PROBE command must be run prior to the macro containing this reference. -- `last_z_result`: Returns the Z result value of the last PROBE - command. Note, if this is used in a macro, due to the order of - template expansion, the PROBE (or similar) command must be run prior - to the macro containing this reference. +- `last_probe_position`: The results of the last `PROBE` command. This + value is encoded as a [coordinate](#accessing-coordinates). The + probe hardware estimates that if one were to command the toolhead to + XY position `last_probe_position.x`,`last_probe_position.y` and + descend then the tip of the toolhead would first contact the bed at + a Z height of `last_probe_position.z`. These coordinates are + relative to the frame (that is, they use the coordinate system + specified in the config file). Note, if this is used in a macro, + due to the order of template expansion, the `PROBE` command must be + run prior to the macro containing this reference. +- `last_z_result`: This value is deprecated; it will be removed in the + near future. ## pwm_cycle_time diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index fcf8cc697..e6471219e 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -49,6 +49,7 @@ class ProbeCommandHelper: gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE, desc=self.cmd_QUERY_PROBE_help) # PROBE command + self.last_probe_position = gcode.Coord((0., 0., 0.)) self.last_z_result = 0. gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) @@ -69,6 +70,7 @@ class ProbeCommandHelper: def get_status(self, eventtime): return {'name': self.name, 'last_query': self.last_state, + 'last_probe_position': self.last_probe_position, 'last_z_result': self.last_z_result} cmd_QUERY_PROBE_help = "Return the status of the z-probe" def cmd_QUERY_PROBE(self, gcmd): @@ -84,8 +86,11 @@ class ProbeCommandHelper: pos = run_single_probe(self.probe, gcmd) gcmd.respond_info("Result: at %.3f,%.3f estimate contact at z=%.6f" % (pos.bed_x, pos.bed_y, pos.bed_z)) + gcode = self.printer.lookup_object('gcode') + self.last_probe_position = gcode.Coord((pos.bed_x, pos.bed_y, + pos.bed_z)) x_offset, y_offset, z_offset = self.probe.get_offsets() - self.last_z_result = pos.bed_z + z_offset # XXX + self.last_z_result = pos.bed_z + z_offset # Deprecated def probe_calibrate_finalize(self, mpresult): if mpresult is None: return From a353efa5b617817988128fbd08b2e294624b0531 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 5 Jan 2026 12:27:03 -0500 Subject: [PATCH 062/108] probe: Return to start XY position on each attempt in PROBE_ACCURACY Signed-off-by: Kevin O'Connor --- klippy/extras/probe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index e6471219e..4f87b8210 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -126,11 +126,11 @@ class ProbeCommandHelper: params = self.probe.get_probe_params(gcmd) sample_count = gcmd.get_int("SAMPLES", 10, minval=1) toolhead = self.printer.lookup_object('toolhead') - pos = toolhead.get_position() + start_pos = toolhead.get_position() gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f" " (samples=%d retract=%.3f" " speed=%.1f lift_speed=%.1f)\n" - % (pos[0], pos[1], pos[2], + % (start_pos[0], start_pos[1], start_pos[2], sample_count, params['sample_retract_dist'], params['probe_speed'], params['lift_speed'])) # Create dummy gcmd with SAMPLES=1 @@ -146,8 +146,8 @@ class ProbeCommandHelper: probe_session.run_probe(fo_gcmd) probe_num += 1 # Retract - pos = toolhead.get_position() - liftpos = [None, None, pos[2] + params['sample_retract_dist']] + lift_z = toolhead.get_position()[2] + params['sample_retract_dist'] + liftpos = [start_pos[0], start_pos[1], lift_z] self._move(liftpos, params['lift_speed']) positions = probe_session.pull_probed_results() probe_session.end_probe_session() From 2956c1e223f14c43da74e501bf28facff9431b70 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 11:22:05 -0500 Subject: [PATCH 063/108] probe: Support passing gcmd to probe.get_offsets() Make it possible for the probe's get_offsets() code to depend on the parameters of the probe request. This is in preparation for eddy "tap" support. Signed-off-by: Kevin O'Connor --- klippy/extras/axis_twist_compensation.py | 10 ++++----- klippy/extras/bed_mesh.py | 4 ++-- klippy/extras/bltouch.py | 4 ++-- klippy/extras/load_cell_probe.py | 4 ++-- klippy/extras/probe.py | 26 ++++++++++++------------ klippy/extras/probe_eddy_current.py | 4 ++-- klippy/extras/smart_effector.py | 4 ++-- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/klippy/extras/axis_twist_compensation.py b/klippy/extras/axis_twist_compensation.py index 28f01d2d6..081af9269 100644 --- a/klippy/extras/axis_twist_compensation.py +++ b/klippy/extras/axis_twist_compensation.py @@ -107,8 +107,7 @@ class Calibrater: self.gcode = self.printer.lookup_object('gcode') self.probe = None # probe settings are set to none, until they are available - self.lift_speed, self.probe_x_offset, self.probe_y_offset, _ = \ - None, None, None, None + self.lift_speed = None self.printer.register_event_handler("klippy:connect", self._handle_connect) self.speed = compensation.speed @@ -135,8 +134,6 @@ class Calibrater: raise self.printer.config_error( "AXIS_TWIST_COMPENSATION requires [probe] to be defined") self.lift_speed = self.probe.get_probe_params()['lift_speed'] - self.probe_x_offset, self.probe_y_offset, _ = \ - self.probe.get_offsets() def _register_gcode_handlers(self): # register gcode handlers @@ -154,6 +151,7 @@ class Calibrater: def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd): self.gcmd = gcmd + probe_x_offset, probe_y_offset, _ = self.probe.get_offsets(gcmd) sample_count = gcmd.get_int('SAMPLE_COUNT', DEFAULT_SAMPLE_COUNT) axis = gcmd.get('AXIS', 'X') @@ -225,7 +223,7 @@ class Calibrater: "Invalid axis.") probe_points = self._calculate_probe_points( - nozzle_points, self.probe_x_offset, self.probe_y_offset) + nozzle_points, probe_x_offset, probe_y_offset) # verify no other manual probe is in progress manual_probe.verify_no_manual_probe(self.printer) @@ -237,7 +235,7 @@ class Calibrater: self._calibration(probe_points, nozzle_points, interval_dist) def _calculate_probe_points(self, nozzle_points, - probe_x_offset, probe_y_offset): + probe_x_offset, probe_y_offset): # calculate the points to put the nozzle at # returned as a list of tuples probe_points = [] diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 822d43efd..9e5b206e9 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -308,7 +308,7 @@ class BedMesh: result["calibration"] = self.bmc.dump_calibration(gcmd) else: result["calibration"] = self.bmc.dump_calibration() - offsets = [0, 0, 0] if prb is None else prb.get_offsets() + offsets = [0, 0, 0] if prb is None else prb.get_offsets(gcmd) result["probe_offsets"] = offsets result["axis_minimum"] = th_sts["axis_minimum"] result["axis_maximum"] = th_sts["axis_maximum"] @@ -1209,7 +1209,7 @@ class RapidScanHelper: gcmd_params["SAMPLE_TIME"] = half_window * 2 self._raise_tool(gcmd, scan_height) probe_session = pprobe.start_probe_session(gcmd) - offsets = pprobe.get_offsets() + offsets = pprobe.get_offsets(gcmd) initial_move = True for pos, is_probe_pt in self.probe_manager.iter_rapid_path(): pos = self._apply_offsets(pos[:2], offsets) diff --git a/klippy/extras/bltouch.py b/klippy/extras/bltouch.py index e39f70dbb..85fd03e9a 100644 --- a/klippy/extras/bltouch.py +++ b/klippy/extras/bltouch.py @@ -80,8 +80,8 @@ class BLTouchProbe: self.handle_connect) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 1bfe738fa..9e5b0475e 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -647,8 +647,8 @@ class LoadCellPrinterProbe: def get_probe_params(self, gcmd=None): return self._param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self._probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self._probe_offsets.get_offsets(gcmd) def start_probe_session(self, gcmd): return self._probe_session.start_probe_session(gcmd) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 4f87b8210..31e5b71e3 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -54,7 +54,7 @@ class ProbeCommandHelper: gcode.register_command('PROBE', self.cmd_PROBE, desc=self.cmd_PROBE_help) # PROBE_CALIBRATE command - self.probe_calibrate_pos = None + self.probe_calibrate_info = None gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, desc=self.cmd_PROBE_CALIBRATE_help) # Other commands @@ -89,13 +89,13 @@ class ProbeCommandHelper: gcode = self.printer.lookup_object('gcode') self.last_probe_position = gcode.Coord((pos.bed_x, pos.bed_y, pos.bed_z)) - x_offset, y_offset, z_offset = self.probe.get_offsets() + x_offset, y_offset, z_offset = self.probe.get_offsets(gcmd) self.last_z_result = pos.bed_z + z_offset # Deprecated def probe_calibrate_finalize(self, mpresult): if mpresult is None: return - x_offset, y_offset, z_offset = self.probe.get_offsets() - z_offset += mpresult.bed_z - self.probe_calibrate_pos.bed_z + ppos, offsets = self.probe_calibrate_info + z_offset = offsets[2] + mpresult.bed_z - ppos.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" @@ -108,17 +108,17 @@ class ProbeCommandHelper: manual_probe.verify_no_manual_probe(self.printer) params = self.probe.get_probe_params(gcmd) # Perform initial probe - pos = run_single_probe(self.probe, gcmd) + ppos = run_single_probe(self.probe, gcmd) # Move away from the bed curpos = self.printer.lookup_object('toolhead').get_position() curpos[2] += 5. self._move(curpos, params['lift_speed']) # Move the nozzle over the probe point - curpos[0] = pos.bed_x - curpos[1] = pos.bed_y + curpos[0] = ppos.bed_x + curpos[1] = ppos.bed_y self._move(curpos, params['probe_speed']) # Start manual probe - self.probe_calibrate_pos = pos + self.probe_calibrate_info = (ppos, self.probe.get_offsets(gcmd)) manual_probe.ManualProbeHelper(self.printer, gcmd, self.probe_calibrate_finalize) cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position" @@ -174,7 +174,7 @@ class ProbeCommandHelper: if offset == 0: gcmd.respond_info("Nothing to do: Z Offset is 0") return - z_offset = self.probe.get_offsets()[2] + z_offset = self.probe.get_offsets(gcmd)[2] new_calibrate = z_offset - offset gcmd.respond_info( "%s: z_offset: %.3f\n" @@ -426,7 +426,7 @@ class ProbeOffsetsHelper: self.x_offset = config.getfloat('x_offset', 0.) self.y_offset = config.getfloat('y_offset', 0.) self.z_offset = config.getfloat('z_offset') - def get_offsets(self): + def get_offsets(self, gcmd=None): return self.x_offset, self.y_offset, self.z_offset def create_probe_result(self, test_pos): return manual_probe.ProbeResult( @@ -509,7 +509,7 @@ class ProbePointsHelper: return # Perform automatic probing self.lift_speed = probe.get_probe_params(gcmd)['lift_speed'] - self.probe_offsets = probe.get_offsets() + self.probe_offsets = probe.get_offsets(gcmd) if self.horizontal_move_z < self.probe_offsets[2]: raise gcmd.error("horizontal_move_z can't be less than" " probe's z_offset") @@ -632,8 +632,8 @@ class PrinterProbe: config, self.param_helper, self.homing_helper.start_probe_session) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 161b675b0..4d97d46d6 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -563,8 +563,8 @@ class PrinterEddyProbe: self.sensor_helper.add_client(cb) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): diff --git a/klippy/extras/smart_effector.py b/klippy/extras/smart_effector.py index 87369f4f3..3422dc247 100644 --- a/klippy/extras/smart_effector.py +++ b/klippy/extras/smart_effector.py @@ -90,8 +90,8 @@ class SmartEffectorProbe: desc=self.cmd_SET_SMART_EFFECTOR_help) def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) - def get_offsets(self): - return self.probe_offsets.get_offsets() + def get_offsets(self, gcmd=None): + return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) def start_probe_session(self, gcmd): From ec08ff5a1e95e3f8fd332362679a1395ac176d08 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 7 Jan 2026 18:37:35 -0500 Subject: [PATCH 064/108] trigger_analog: Rename load_cell_probe.c to trigger_analog.c Rename the mcu based load_cell_probe code to trigger_analog. This is a rename of the C code files, struct names, and command names. There is no change in behavior (other than naming) with this change. This is in preparation for using the load_cell_probe functionality with other sensors. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 6 +- klippy/extras/hx71x.py | 6 +- klippy/extras/load_cell_probe.py | 52 +++--- src/Kconfig | 4 +- src/Makefile | 2 +- src/load_cell_probe.c | 298 ------------------------------- src/load_cell_probe.h | 10 -- src/sensor_ads1220.c | 20 +-- src/sensor_hx71x.c | 22 +-- src/trigger_analog.c | 294 ++++++++++++++++++++++++++++++ src/trigger_analog.h | 9 + 11 files changed, 359 insertions(+), 364 deletions(-) delete mode 100644 src/load_cell_probe.c delete mode 100644 src/load_cell_probe.h create mode 100644 src/trigger_analog.c create mode 100644 src/trigger_analog.h diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 5e9ef72ba..fda584ac3 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -110,7 +110,7 @@ class ADS1220: self.query_ads1220_cmd = self.mcu.lookup_command( "query_ads1220 oid=%c rest_ticks=%u", cq=cmdqueue) self.attach_probe_cmd = self.mcu.lookup_command( - "ads1220_attach_load_cell_probe oid=%c load_cell_probe_oid=%c") + "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c") self.ffreader.setup_query_command("query_ads1220_status oid=%c", oid=self.oid, cq=cmdqueue) @@ -129,8 +129,8 @@ class ADS1220: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_load_cell_probe(self, load_cell_probe_oid): - self.attach_probe_cmd.send([self.oid, load_cell_probe_oid]) + def attach_trigger_analog(self, trigger_analog_oid): + self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) # Measurement decoding def _convert_samples(self, samples): diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index a7f49f8ad..60fe6047c 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -66,7 +66,7 @@ class HX71xBase: self.query_hx71x_cmd = self.mcu.lookup_command( "query_hx71x oid=%c rest_ticks=%u") self.attach_probe_cmd = self.mcu.lookup_command( - "hx71x_attach_load_cell_probe oid=%c load_cell_probe_oid=%c") + "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c") self.ffreader.setup_query_command("query_hx71x_status oid=%c", oid=self.oid, cq=self.mcu.alloc_command_queue()) @@ -87,8 +87,8 @@ class HX71xBase: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_load_cell_probe(self, load_cell_probe_oid): - self.attach_probe_cmd.send([self.oid, load_cell_probe_oid]) + def attach_trigger_analog(self, trigger_analog_oid): + self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) # Measurement decoding def _convert_samples(self, samples): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 9e5b0475e..9414ffc54 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -295,9 +295,9 @@ class LoadCellProbeConfigHelper: return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_INT_BITS) -# McuLoadCellProbe is the interface to `load_cell_probe` on the MCU +# MCU_trigger_analog is the interface to `trigger_analog` on the MCU # This also manages the SosFilter so all commands use one command queue -class McuLoadCellProbe: +class MCU_trigger_analog: WATCHDOG_MAX = 3 ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 @@ -325,27 +325,27 @@ class McuLoadCellProbe: def _config_commands(self): self._sos_filter.create_filter() self._mcu.add_config_cmd( - "config_load_cell_probe oid=%d sos_filter_oid=%d" % ( + "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) def _build_config(self): # Lookup commands self._query_cmd = self._mcu.lookup_query_command( - "load_cell_probe_query_state oid=%c", - "load_cell_probe_state oid=%c is_homing_trigger=%c " + "trigger_analog_query_state oid=%c", + "trigger_analog_state oid=%c is_homing_trigger=%c " "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) self._set_range_cmd = self._mcu.lookup_command( - "load_cell_probe_set_range" + "trigger_analog_set_range" " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" " trigger_grams=%u grams_per_count=%i", cq=self._cmd_queue) self._home_cmd = self._mcu.lookup_command( - "load_cell_probe_home oid=%c trsync_oid=%c trigger_reason=%c" + "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" " error_reason=%c clock=%u rest_ticks=%u timeout=%u", cq=self._cmd_queue) # the sensor data stream is connected on the MCU at the ready event def _on_connect(self): - self._sensor.attach_load_cell_probe(self._oid) + self._sensor.attach_trigger_analog(self._oid) def get_oid(self): return self._oid @@ -387,30 +387,30 @@ class McuLoadCellProbe: return self._mcu.clock_to_print_time(trigger_ticks) -# Execute probing moves using the McuLoadCellProbe +# Execute probing moves using the MCU_trigger_analog class LoadCellProbingMove: ERROR_MAP = { mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " "homing", - McuLoadCellProbe.ERROR_SAFETY_RANGE: "Load Cell Probe Error: load " - "exceeds safety limit", - McuLoadCellProbe.ERROR_OVERFLOW: "Load Cell Probe Error: fixed point " - "math overflow", - McuLoadCellProbe.ERROR_WATCHDOG: "Load Cell Probe Error: timed out " - "waiting for sensor data" + MCU_trigger_analog.ERROR_SAFETY_RANGE: "Load Cell Probe Error: load " + "exceeds safety limit", + MCU_trigger_analog.ERROR_OVERFLOW: "Load Cell Probe Error: fixed point " + "math overflow", + MCU_trigger_analog.ERROR_WATCHDOG: "Load Cell Probe Error: timed out " + "waiting for sensor data" } - def __init__(self, config, mcu_load_cell_probe, param_helper, + def __init__(self, config, mcu_trigger_analog, param_helper, continuous_tare_filter_helper, config_helper): self._printer = config.get_printer() - self._mcu_load_cell_probe = mcu_load_cell_probe + self._mcu_trigger_analog = mcu_trigger_analog self._param_helper = param_helper self._continuous_tare_filter_helper = continuous_tare_filter_helper self._config_helper = config_helper - self._mcu = mcu_load_cell_probe.get_mcu() - self._load_cell = mcu_load_cell_probe.get_load_cell() + self._mcu = mcu_trigger_analog.get_mcu() + self._load_cell = mcu_trigger_analog.get_load_cell() self._z_min_position = probe.lookup_minimum_z(config) - self._dispatch = mcu_load_cell_probe.get_dispatch() + self._dispatch = mcu_trigger_analog.get_dispatch() probe.LookupZSteppers(config, self._dispatch.add_stepper) # internal state tracking self._tare_counts = 0 @@ -436,12 +436,12 @@ class LoadCellProbingMove: tare_counts = np.average(np.array(tare_samples)[:, 2].astype(float)) # update sos_filter with any gcode parameter changes self._continuous_tare_filter_helper.update_from_command(gcmd) - self._mcu_load_cell_probe.set_endstop_range(tare_counts, gcmd) + self._mcu_trigger_analog.set_endstop_range(tare_counts, gcmd) def _home_start(self, print_time): # start trsync trigger_completion = self._dispatch.start(print_time) - self._mcu_load_cell_probe.home_start(print_time) + self._mcu_trigger_analog.home_start(print_time) return trigger_completion def home_start(self, print_time, sample_time, sample_count, rest_time, @@ -453,7 +453,7 @@ class LoadCellProbingMove: # trigger has happened, now to find out why... res = self._dispatch.stop() # clear the homing state so it stops processing samples - self._last_trigger_time = self._mcu_load_cell_probe.clear_home() + self._last_trigger_time = self._mcu_trigger_analog.clear_home() if self._mcu.is_fileoutput(): self._last_trigger_time = home_end_time if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: @@ -468,7 +468,7 @@ class LoadCellProbingMove: def get_steppers(self): return self._dispatch.get_steppers() - # Probe towards z_min until the load_cell_probe on the MCU triggers + # Probe towards z_min until the trigger_analog on the MCU triggers def probing_move(self, gcmd): # do not permit probing if the load cell is not calibrated if not self._load_cell.is_calibrated(): @@ -627,11 +627,11 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_load_cell_probe = McuLoadCellProbe(config, self._load_cell, + self._mcu_trigger_analog = MCU_trigger_analog(config, self._load_cell, continuous_tare_filter_helper.get_sos_filter(), config_helper, trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, - self._mcu_load_cell_probe, self._param_helper, + self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) self._tapping_move = TappingMove(config, load_cell_probing_move, config_helper) diff --git a/src/Kconfig b/src/Kconfig index 6740946d8..8013b40f0 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -177,13 +177,13 @@ config NEED_SENSOR_BULK depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 || WANT_ICM20948 \ || WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 || WANT_SENSOR_ANGLE default y -config WANT_LOAD_CELL_PROBE +config WANT_TRIGGER_ANALOG bool depends on WANT_HX71X || WANT_ADS1220 default y config NEED_SOS_FILTER bool - depends on WANT_LOAD_CELL_PROBE + depends on WANT_TRIGGER_ANALOG default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE diff --git a/src/Makefile b/src/Makefile index 974204bc5..dc6d4db52 100644 --- a/src/Makefile +++ b/src/Makefile @@ -28,4 +28,4 @@ src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c src-$(CONFIG_WANT_SENSOR_ANGLE) += sensor_angle.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c src-$(CONFIG_NEED_SOS_FILTER) += sos_filter.c -src-$(CONFIG_WANT_LOAD_CELL_PROBE) += load_cell_probe.c +src-$(CONFIG_WANT_TRIGGER_ANALOG) += trigger_analog.c diff --git a/src/load_cell_probe.c b/src/load_cell_probe.c deleted file mode 100644 index 48da77937..000000000 --- a/src/load_cell_probe.c +++ /dev/null @@ -1,298 +0,0 @@ -// Load Cell based end stops. -// -// Copyright (C) 2025 Gareth Farrington -// -// This file may be distributed under the terms of the GNU GPLv3 license. - -#include "basecmd.h" // oid_alloc -#include "command.h" // DECL_COMMAND -#include "sched.h" // shutdown -#include "trsync.h" // trsync_do_trigger -#include "board/misc.h" // timer_read_time -#include "sos_filter.h" // fixedQ12_t -#include "load_cell_probe.h" //load_cell_probe_report_sample -#include // int32_t -#include // abs - -// Q2.29 -typedef int32_t fixedQ2_t; -#define FIXEDQ2 2 -#define FIXEDQ2_FRAC_BITS ((32 - FIXEDQ2) - 1) - -// Q32.29 - a Q2.29 value stored in int64 -typedef int64_t fixedQ32_t; -#define FIXEDQ32_FRAC_BITS FIXEDQ2_FRAC_BITS - -// Q16.15 -typedef int32_t fixedQ16_t; -#define FIXEDQ16 16 -#define FIXEDQ16_FRAC_BITS ((32 - FIXEDQ16) - 1) - -// Q48.15 - a Q16.15 value stored in int64 -typedef int64_t fixedQ48_t; -#define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS - -#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) -#define ERROR_SAFETY_RANGE 0 -#define ERROR_OVERFLOW 1 -#define ERROR_WATCHDOG 2 - -// Flags -enum {FLAG_IS_HOMING = 1 << 0 - , FLAG_IS_HOMING_TRIGGER = 1 << 1 - , FLAG_AWAIT_HOMING = 1 << 2 - }; - -// Endstop Structure -struct load_cell_probe { - struct timer time; - uint32_t trigger_grams, trigger_ticks, last_sample_ticks, rest_ticks; - uint32_t homing_start_time; - struct trsync *ts; - int32_t safety_counts_min, safety_counts_max, tare_counts; - uint8_t flags, trigger_reason, error_reason, watchdog_max - , watchdog_count; - fixedQ16_t trigger_grams_fixed; - fixedQ2_t grams_per_count; - struct sos_filter *sf; -}; - -static inline uint8_t -overflows_int32(int64_t value) { - return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; -} - -// returns the integer part of a fixedQ48_t -static inline int64_t -round_fixedQ48(const int64_t fixed_value) { - return fixed_value >> FIXEDQ48_FRAC_BITS; -} - -// Convert sensor counts to grams -static inline fixedQ48_t -counts_to_grams(struct load_cell_probe *lce, const int32_t counts) { - // tearing ensures readings are referenced to 0.0g - const int32_t delta = counts - lce->tare_counts; - // convert sensor counts to grams by multiplication: 124 * 0.051 = 6.324 - // this optimizes to single cycle SMULL instruction - const fixedQ32_t product = (int64_t)delta * (int64_t)lce->grams_per_count; - // after multiplication there are 30 fraction bits, reduce to 15 - // caller verifies this wont overflow a 32bit int when truncated - const fixedQ48_t grams = product >> - (FIXEDQ32_FRAC_BITS - FIXEDQ48_FRAC_BITS); - return grams; -} - -static inline uint8_t -is_flag_set(const uint8_t mask, struct load_cell_probe *lce) -{ - return !!(mask & lce->flags); -} - -static inline void -set_flag(uint8_t mask, struct load_cell_probe *lce) -{ - lce->flags |= mask; -} - -static inline void -clear_flag(uint8_t mask, struct load_cell_probe *lce) -{ - lce->flags &= ~mask; -} - -void -try_trigger(struct load_cell_probe *lce, uint32_t ticks) -{ - uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, lce); - if (!is_homing_triggered) { - // the first triggering sample when homing sets the trigger time - lce->trigger_ticks = ticks; - // this flag latches until a reset, disabling further triggering - set_flag(FLAG_IS_HOMING_TRIGGER, lce); - trsync_do_trigger(lce->ts, lce->trigger_reason); - } -} - -void -trigger_error(struct load_cell_probe *lce, uint8_t error_code) -{ - trsync_do_trigger(lce->ts, lce->error_reason + error_code); -} - -// Used by Sensors to report new raw ADC sample -void -load_cell_probe_report_sample(struct load_cell_probe *lce - , const int32_t sample) -{ - // only process samples when homing - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, lce); - if (!is_homing) { - return; - } - - // save new sample - uint32_t ticks = timer_read_time(); - lce->last_sample_ticks = ticks; - lce->watchdog_count = 0; - - // do not trigger before homing start time - uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, lce); - if (await_homing && timer_is_before(ticks, lce->homing_start_time)) { - return; - } - clear_flag(FLAG_AWAIT_HOMING, lce); - - // check for safety limit violations - const uint8_t is_safety_trigger = sample <= lce->safety_counts_min - || sample >= lce->safety_counts_max; - // too much force, this is an error while homing - if (is_safety_trigger) { - trigger_error(lce, ERROR_SAFETY_RANGE); - return; - } - - // convert sample to grams - const fixedQ48_t raw_grams = counts_to_grams(lce, sample); - if (overflows_int32(raw_grams)) { - trigger_error(lce, ERROR_OVERFLOW); - return; - } - - // perform filtering - const fixedQ16_t filtered_grams = sosfilt(lce->sf, (fixedQ16_t)raw_grams); - - // update trigger state - if (abs(filtered_grams) >= lce->trigger_grams_fixed) { - try_trigger(lce, lce->last_sample_ticks); - } -} - -// Timer callback that monitors for timeouts -static uint_fast8_t -watchdog_event(struct timer *t) -{ - struct load_cell_probe *lce = container_of(t, struct load_cell_probe - , time); - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, lce); - uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, lce); - // the watchdog stops when not homing or when trsync becomes triggered - if (!is_homing || is_homing_trigger) { - return SF_DONE; - } - - if (lce->watchdog_count > lce->watchdog_max) { - trigger_error(lce, ERROR_WATCHDOG); - } - lce->watchdog_count += 1; - - // A sample was recently delivered, continue monitoring - lce->time.waketime += lce->rest_ticks; - return SF_RESCHEDULE; -} - -static void -set_endstop_range(struct load_cell_probe *lce - , int32_t safety_counts_min, int32_t safety_counts_max - , int32_t tare_counts, uint32_t trigger_grams - , fixedQ2_t grams_per_count) -{ - if (!(safety_counts_max >= safety_counts_min)) { - shutdown("Safety range reversed"); - } - if (trigger_grams > MAX_TRIGGER_GRAMS) { - shutdown("trigger_grams too large"); - } - // grams_per_count must be a positive fraction in Q2 format - const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; - if (grams_per_count < 0 || grams_per_count >= one) { - shutdown("grams_per_count is invalid"); - } - lce->safety_counts_min = safety_counts_min; - lce->safety_counts_max = safety_counts_max; - lce->tare_counts = tare_counts; - lce->trigger_grams = trigger_grams; - lce->trigger_grams_fixed = trigger_grams << FIXEDQ16_FRAC_BITS; - lce->grams_per_count = grams_per_count; -} - -// Create a load_cell_probe -void -command_config_load_cell_probe(uint32_t *args) -{ - struct load_cell_probe *lce = oid_alloc(args[0] - , command_config_load_cell_probe, sizeof(*lce)); - lce->flags = 0; - lce->trigger_ticks = 0; - lce->watchdog_max = 0; - lce->watchdog_count = 0; - lce->sf = sos_filter_oid_lookup(args[1]); - set_endstop_range(lce, 0, 0, 0, 0, 0); -} -DECL_COMMAND(command_config_load_cell_probe, "config_load_cell_probe" - " oid=%c sos_filter_oid=%c"); - -// Lookup a load_cell_probe -struct load_cell_probe * -load_cell_probe_oid_lookup(uint8_t oid) -{ - return oid_lookup(oid, command_config_load_cell_probe); -} - -// Set the triggering range and tare value -void -command_load_cell_probe_set_range(uint32_t *args) -{ - struct load_cell_probe *lce = load_cell_probe_oid_lookup(args[0]); - set_endstop_range(lce, args[1], args[2], args[3], args[4] - , (fixedQ16_t)args[5]); -} -DECL_COMMAND(command_load_cell_probe_set_range, "load_cell_probe_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" - " trigger_grams=%u grams_per_count=%i"); - -// Home an axis -void -command_load_cell_probe_home(uint32_t *args) -{ - struct load_cell_probe *lce = load_cell_probe_oid_lookup(args[0]); - sched_del_timer(&lce->time); - // clear the homing trigger flag - clear_flag(FLAG_IS_HOMING_TRIGGER, lce); - clear_flag(FLAG_IS_HOMING, lce); - lce->trigger_ticks = 0; - lce->ts = NULL; - // 0 samples indicates homing is finished - if (args[3] == 0) { - // Disable end stop checking - return; - } - lce->ts = trsync_oid_lookup(args[1]); - lce->trigger_reason = args[2]; - lce->error_reason = args[3]; - lce->time.waketime = args[4]; - lce->homing_start_time = args[4]; - lce->rest_ticks = args[5]; - lce->watchdog_max = args[6]; - lce->watchdog_count = 0; - lce->time.func = watchdog_event; - set_flag(FLAG_IS_HOMING, lce); - set_flag(FLAG_AWAIT_HOMING, lce); - sched_add_timer(&lce->time); -} -DECL_COMMAND(command_load_cell_probe_home, - "load_cell_probe_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); - -void -command_load_cell_probe_query_state(uint32_t *args) -{ - uint8_t oid = args[0]; - struct load_cell_probe *lce = load_cell_probe_oid_lookup(args[0]); - sendf("load_cell_probe_state oid=%c is_homing_trigger=%c trigger_ticks=%u" - , oid - , is_flag_set(FLAG_IS_HOMING_TRIGGER, lce) - , lce->trigger_ticks); -} -DECL_COMMAND(command_load_cell_probe_query_state - , "load_cell_probe_query_state oid=%c"); diff --git a/src/load_cell_probe.h b/src/load_cell_probe.h deleted file mode 100644 index e67c16e55..000000000 --- a/src/load_cell_probe.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __LOAD_CELL_PROBE_H -#define __LOAD_CELL_PROBE_H - -#include // uint8_t - -struct load_cell_probe *load_cell_probe_oid_lookup(uint8_t oid); -void load_cell_probe_report_sample(struct load_cell_probe *lce - , int32_t sample); - -#endif // load_cell_probe.h diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c index f51dc355a..93d52b6ae 100644 --- a/src/sensor_ads1220.c +++ b/src/sensor_ads1220.c @@ -4,16 +4,16 @@ // // This file may be distributed under the terms of the GNU GPLv3 license. +#include +#include "basecmd.h" // oid_alloc #include "board/irq.h" // irq_disable #include "board/gpio.h" // gpio_out_write #include "board/misc.h" // timer_read_time -#include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // sched_add_timer #include "sensor_bulk.h" // sensor_bulk_report -#include "load_cell_probe.h" // load_cell_probe_report_sample #include "spicmds.h" // spidev_transfer -#include +#include "trigger_analog.h" // trigger_analog_update struct ads1220_adc { struct timer timer; @@ -22,7 +22,7 @@ struct ads1220_adc { struct spidev_s *spi; uint8_t pending_flag, data_count; struct sensor_bulk sb; - struct load_cell_probe *lce; + struct trigger_analog *ta; }; // Flag types @@ -97,8 +97,8 @@ ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) counts |= 0xFF000000; // endstop is optional, report if enabled and no errors - if (ads1220->lce) { - load_cell_probe_report_sample(ads1220->lce, counts); + if (ads1220->ta) { + trigger_analog_update(ads1220->ta, counts); } add_sample(ads1220, oid, counts); @@ -119,13 +119,13 @@ DECL_COMMAND(command_config_ads1220, "config_ads1220 oid=%c" " spi_oid=%c data_ready_pin=%u"); void -ads1220_attach_load_cell_probe(uint32_t *args) { +ads1220_attach_trigger_analog(uint32_t *args) { uint8_t oid = args[0]; struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220); - ads1220->lce = load_cell_probe_oid_lookup(args[1]); + ads1220->ta = trigger_analog_oid_lookup(args[1]); } -DECL_COMMAND(ads1220_attach_load_cell_probe, - "ads1220_attach_load_cell_probe oid=%c load_cell_probe_oid=%c"); +DECL_COMMAND(ads1220_attach_trigger_analog, + "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c"); // start/stop capturing ADC data void diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 74575eec1..2c09e97af 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -4,17 +4,17 @@ // // This file may be distributed under the terms of the GNU GPLv3 license. +#include +#include #include "autoconf.h" // CONFIG_MACH_AVR +#include "basecmd.h" // oid_alloc #include "board/gpio.h" // gpio_out_write #include "board/irq.h" // irq_poll #include "board/misc.h" // timer_read_time -#include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // sched_add_timer #include "sensor_bulk.h" // sensor_bulk_report -#include "load_cell_probe.h" // load_cell_probe_report_sample -#include -#include +#include "trigger_analog.h" // trigger_analog_update struct hx71x_adc { struct timer timer; @@ -25,7 +25,7 @@ struct hx71x_adc { struct gpio_in dout; // pin used to receive data from the hx71x struct gpio_out sclk; // pin used to generate clock for the hx71x struct sensor_bulk sb; - struct load_cell_probe *lce; + struct trigger_analog *ta; }; enum { @@ -178,8 +178,8 @@ hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) } // probe is optional, report if enabled - if (hx71x->last_error == 0 && hx71x->lce) { - load_cell_probe_report_sample(hx71x->lce, counts); + if (hx71x->last_error == 0 && hx71x->ta) { + trigger_analog_update(hx71x->ta, counts); } // Add measurement to buffer @@ -206,13 +206,13 @@ DECL_COMMAND(command_config_hx71x, "config_hx71x oid=%c gain_channel=%c" " dout_pin=%u sclk_pin=%u"); void -hx71x_attach_load_cell_probe(uint32_t *args) { +hx71x_attach_trigger_analog(uint32_t *args) { uint8_t oid = args[0]; struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x); - hx71x->lce = load_cell_probe_oid_lookup(args[1]); + hx71x->ta = trigger_analog_oid_lookup(args[1]); } -DECL_COMMAND(hx71x_attach_load_cell_probe, "hx71x_attach_load_cell_probe oid=%c" - " load_cell_probe_oid=%c"); +DECL_COMMAND(hx71x_attach_trigger_analog, "hx71x_attach_trigger_analog oid=%c" + " trigger_analog_oid=%c"); // start/stop capturing ADC data void diff --git a/src/trigger_analog.c b/src/trigger_analog.c new file mode 100644 index 000000000..9c0220e19 --- /dev/null +++ b/src/trigger_analog.c @@ -0,0 +1,294 @@ +// Support homing/probing "trigger" notification from analog sensors +// +// Copyright (C) 2025 Gareth Farrington +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include // abs +#include "basecmd.h" // oid_alloc +#include "board/misc.h" // timer_read_time +#include "command.h" // DECL_COMMAND +#include "sched.h" // shutdown +#include "sos_filter.h" // fixedQ12_t +#include "trigger_analog.h" // trigger_analog_update +#include "trsync.h" // trsync_do_trigger + +// Q2.29 +typedef int32_t fixedQ2_t; +#define FIXEDQ2 2 +#define FIXEDQ2_FRAC_BITS ((32 - FIXEDQ2) - 1) + +// Q32.29 - a Q2.29 value stored in int64 +typedef int64_t fixedQ32_t; +#define FIXEDQ32_FRAC_BITS FIXEDQ2_FRAC_BITS + +// Q16.15 +typedef int32_t fixedQ16_t; +#define FIXEDQ16 16 +#define FIXEDQ16_FRAC_BITS ((32 - FIXEDQ16) - 1) + +// Q48.15 - a Q16.15 value stored in int64 +typedef int64_t fixedQ48_t; +#define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS + +#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) +#define ERROR_SAFETY_RANGE 0 +#define ERROR_OVERFLOW 1 +#define ERROR_WATCHDOG 2 + +// Flags +enum {FLAG_IS_HOMING = 1 << 0 + , FLAG_IS_HOMING_TRIGGER = 1 << 1 + , FLAG_AWAIT_HOMING = 1 << 2 +}; + +// Endstop Structure +struct trigger_analog { + struct timer time; + uint32_t trigger_grams, trigger_ticks, last_sample_ticks, rest_ticks; + uint32_t homing_start_time; + struct trsync *ts; + int32_t safety_counts_min, safety_counts_max, tare_counts; + uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; + fixedQ16_t trigger_grams_fixed; + fixedQ2_t grams_per_count; + struct sos_filter *sf; +}; + +static inline uint8_t +overflows_int32(int64_t value) { + return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; +} + +// returns the integer part of a fixedQ48_t +static inline int64_t +round_fixedQ48(const int64_t fixed_value) { + return fixed_value >> FIXEDQ48_FRAC_BITS; +} + +// Convert sensor counts to grams +static inline fixedQ48_t +counts_to_grams(struct trigger_analog *ta, const int32_t counts) { + // tearing ensures readings are referenced to 0.0g + const int32_t delta = counts - ta->tare_counts; + // convert sensor counts to grams by multiplication: 124 * 0.051 = 6.324 + // this optimizes to single cycle SMULL instruction + const fixedQ32_t product = (int64_t)delta * (int64_t)ta->grams_per_count; + // after multiplication there are 30 fraction bits, reduce to 15 + // caller verifies this wont overflow a 32bit int when truncated + const fixedQ48_t grams = product >> + (FIXEDQ32_FRAC_BITS - FIXEDQ48_FRAC_BITS); + return grams; +} + +static inline uint8_t +is_flag_set(const uint8_t mask, struct trigger_analog *ta) +{ + return !!(mask & ta->flags); +} + +static inline void +set_flag(uint8_t mask, struct trigger_analog *ta) +{ + ta->flags |= mask; +} + +static inline void +clear_flag(uint8_t mask, struct trigger_analog *ta) +{ + ta->flags &= ~mask; +} + +void +try_trigger(struct trigger_analog *ta, uint32_t ticks) +{ + uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); + if (!is_homing_triggered) { + // the first triggering sample when homing sets the trigger time + ta->trigger_ticks = ticks; + // this flag latches until a reset, disabling further triggering + set_flag(FLAG_IS_HOMING_TRIGGER, ta); + trsync_do_trigger(ta->ts, ta->trigger_reason); + } +} + +void +trigger_error(struct trigger_analog *ta, uint8_t error_code) +{ + trsync_do_trigger(ta->ts, ta->error_reason + error_code); +} + +// Used by Sensors to report new raw ADC sample +void +trigger_analog_update(struct trigger_analog *ta, const int32_t sample) +{ + // only process samples when homing + uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); + if (!is_homing) { + return; + } + + // save new sample + uint32_t ticks = timer_read_time(); + ta->last_sample_ticks = ticks; + ta->watchdog_count = 0; + + // do not trigger before homing start time + uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, ta); + if (await_homing && timer_is_before(ticks, ta->homing_start_time)) { + return; + } + clear_flag(FLAG_AWAIT_HOMING, ta); + + // check for safety limit violations + const uint8_t is_safety_trigger = sample <= ta->safety_counts_min + || sample >= ta->safety_counts_max; + // too much force, this is an error while homing + if (is_safety_trigger) { + trigger_error(ta, ERROR_SAFETY_RANGE); + return; + } + + // convert sample to grams + const fixedQ48_t raw_grams = counts_to_grams(ta, sample); + if (overflows_int32(raw_grams)) { + trigger_error(ta, ERROR_OVERFLOW); + return; + } + + // perform filtering + const fixedQ16_t filtered_grams = sosfilt(ta->sf, (fixedQ16_t)raw_grams); + + // update trigger state + if (abs(filtered_grams) >= ta->trigger_grams_fixed) { + try_trigger(ta, ta->last_sample_ticks); + } +} + +// Timer callback that monitors for timeouts +static uint_fast8_t +watchdog_event(struct timer *t) +{ + struct trigger_analog *ta = container_of(t, struct trigger_analog, time); + uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); + uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); + // the watchdog stops when not homing or when trsync becomes triggered + if (!is_homing || is_homing_trigger) { + return SF_DONE; + } + + if (ta->watchdog_count > ta->watchdog_max) { + trigger_error(ta, ERROR_WATCHDOG); + } + ta->watchdog_count += 1; + + // A sample was recently delivered, continue monitoring + ta->time.waketime += ta->rest_ticks; + return SF_RESCHEDULE; +} + +static void +set_endstop_range(struct trigger_analog *ta + , int32_t safety_counts_min, int32_t safety_counts_max + , int32_t tare_counts, uint32_t trigger_grams + , fixedQ2_t grams_per_count) +{ + if (!(safety_counts_max >= safety_counts_min)) { + shutdown("Safety range reversed"); + } + if (trigger_grams > MAX_TRIGGER_GRAMS) { + shutdown("trigger_grams too large"); + } + // grams_per_count must be a positive fraction in Q2 format + const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; + if (grams_per_count < 0 || grams_per_count >= one) { + shutdown("grams_per_count is invalid"); + } + ta->safety_counts_min = safety_counts_min; + ta->safety_counts_max = safety_counts_max; + ta->tare_counts = tare_counts; + ta->trigger_grams = trigger_grams; + ta->trigger_grams_fixed = trigger_grams << FIXEDQ16_FRAC_BITS; + ta->grams_per_count = grams_per_count; +} + +// Create a trigger_analog +void +command_config_trigger_analog(uint32_t *args) +{ + struct trigger_analog *ta = oid_alloc( + args[0], command_config_trigger_analog, sizeof(*ta)); + ta->flags = 0; + ta->trigger_ticks = 0; + ta->watchdog_max = 0; + ta->watchdog_count = 0; + ta->sf = sos_filter_oid_lookup(args[1]); + set_endstop_range(ta, 0, 0, 0, 0, 0); +} +DECL_COMMAND(command_config_trigger_analog + , "config_trigger_analog oid=%c sos_filter_oid=%c"); + +// Lookup a trigger_analog +struct trigger_analog * +trigger_analog_oid_lookup(uint8_t oid) +{ + return oid_lookup(oid, command_config_trigger_analog); +} + +// Set the triggering range and tare value +void +command_trigger_analog_set_range(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + set_endstop_range(ta, args[1], args[2], args[3], args[4] + , (fixedQ16_t)args[5]); +} +DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" + " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" + " trigger_grams=%u grams_per_count=%i"); + +// Home an axis +void +command_trigger_analog_home(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + sched_del_timer(&ta->time); + // clear the homing trigger flag + clear_flag(FLAG_IS_HOMING_TRIGGER, ta); + clear_flag(FLAG_IS_HOMING, ta); + ta->trigger_ticks = 0; + ta->ts = NULL; + // 0 samples indicates homing is finished + if (args[3] == 0) { + // Disable end stop checking + return; + } + ta->ts = trsync_oid_lookup(args[1]); + ta->trigger_reason = args[2]; + ta->error_reason = args[3]; + ta->time.waketime = args[4]; + ta->homing_start_time = args[4]; + ta->rest_ticks = args[5]; + ta->watchdog_max = args[6]; + ta->watchdog_count = 0; + ta->time.func = watchdog_event; + set_flag(FLAG_IS_HOMING, ta); + set_flag(FLAG_AWAIT_HOMING, ta); + sched_add_timer(&ta->time); +} +DECL_COMMAND(command_trigger_analog_home, + "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" + " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); + +void +command_trigger_analog_query_state(uint32_t *args) +{ + uint8_t oid = args[0]; + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + sendf("trigger_analog_state oid=%c is_homing_trigger=%c trigger_ticks=%u" + , oid + , is_flag_set(FLAG_IS_HOMING_TRIGGER, ta) + , ta->trigger_ticks); +} +DECL_COMMAND(command_trigger_analog_query_state + , "trigger_analog_query_state oid=%c"); diff --git a/src/trigger_analog.h b/src/trigger_analog.h new file mode 100644 index 000000000..9867095e8 --- /dev/null +++ b/src/trigger_analog.h @@ -0,0 +1,9 @@ +#ifndef __TRIGGER_ANALOG_H +#define __TRIGGER_ANALOG_H + +#include // uint8_t + +struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid); +void trigger_analog_update(struct trigger_analog *ta, int32_t sample); + +#endif // trigger_analog.h From 667c57e444fceacfa3381e0a4558efab6d86d9ce Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 13:33:04 -0500 Subject: [PATCH 065/108] sos_filter: Remove unnecessary is_active flag It's reasonable to deactivate the filter by setting n_sections=0, and this makes the code a little easier to use when the host doesn't actually need any filtering. Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 6e64bc0a2..2ed2b85a2 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -23,7 +23,7 @@ struct sos_filter_section { }; struct sos_filter { - uint8_t max_sections, n_sections, coeff_frac_bits, is_active; + uint8_t max_sections, n_sections, coeff_frac_bits; uint32_t coeff_rounding; // filter composed of second order sections struct sos_filter_section filter[0]; @@ -56,15 +56,6 @@ fixed_mul(struct sos_filter *sf, const fixedQ_coeff_t coeff // returns the fixedQ_value_t filtered value int32_t sosfilt(struct sos_filter *sf, const int32_t unfiltered_value) { - if (!sf->is_active) { - shutdown("sos_filter not property initialized"); - } - - // an empty filter performs no filtering - if (sf->n_sections == 0) { - return unfiltered_value; - } - fixedQ_value_t cur_val = unfiltered_value; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { @@ -92,7 +83,6 @@ command_config_sos_filter(uint32_t *args) struct sos_filter *sf = oid_alloc(args[0] , command_config_sos_filter, size); sf->max_sections = max_sections; - sf->is_active = 0; } DECL_COMMAND(command_config_sos_filter, "config_sos_filter oid=%c" " max_sections=%c"); @@ -118,7 +108,7 @@ command_sos_filter_set_section(uint32_t *args) { struct sos_filter *sf = sos_filter_oid_lookup(args[0]); // setting a section marks the filter as inactive - sf->is_active = 0; + sf->n_sections = 0; uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); // copy section data @@ -137,7 +127,7 @@ command_sos_filter_set_state(uint32_t *args) { struct sos_filter *sf = sos_filter_oid_lookup(args[0]); // setting a section's state marks the filter as inactive - sf->is_active = 0; + sf->n_sections = 0; // copy state data uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); @@ -160,8 +150,6 @@ command_sos_filter_activate(uint32_t *args) const uint8_t coeff_int_bits = args[2]; sf->coeff_frac_bits = (31 - coeff_int_bits); sf->coeff_rounding = (1 << (sf->coeff_frac_bits - 1)); - // mark filter as ready to use - sf->is_active = 1; } DECL_COMMAND(command_sos_filter_activate , "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c"); From fd195ff4ce0646403846efb1890541cec4d87d37 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 13:35:13 -0500 Subject: [PATCH 066/108] sos_filter: Remove unnecessary "const" declarations A "const" declaration on a local integer does not do anything in C code. Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 16 +++++++++------- src/sos_filter.h | 3 +-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 2ed2b85a2..96281e282 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -30,14 +30,15 @@ struct sos_filter { }; static inline uint8_t -overflows_int32(int64_t value) { +overflows_int32(int64_t value) +{ return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; } // Multiply a coefficient in fixedQ_coeff_t by a value fixedQ_value_t static inline fixedQ_value_t -fixed_mul(struct sos_filter *sf, const fixedQ_coeff_t coeff - , const fixedQ_value_t value) { +fixed_mul(struct sos_filter *sf, fixedQ_coeff_t coeff, fixedQ_value_t value) +{ // This optimizes to single cycle SMULL on Arm Coretex M0+ int64_t product = (int64_t)coeff * (int64_t)value; // round up at the last bit to be shifted away @@ -55,7 +56,8 @@ fixed_mul(struct sos_filter *sf, const fixedQ_coeff_t coeff // Apply the sosfilt algorithm to a new datapoint // returns the fixedQ_value_t filtered value int32_t -sosfilt(struct sos_filter *sf, const int32_t unfiltered_value) { +sosfilt(struct sos_filter *sf, int32_t unfiltered_value) +{ fixedQ_value_t cur_val = unfiltered_value; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { @@ -112,7 +114,7 @@ command_sos_filter_set_section(uint32_t *args) uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); // copy section data - const uint8_t arg_base = 2; + uint8_t arg_base = 2; for (uint8_t i = 0; i < SECTION_WIDTH; i++) { sf->filter[section_idx].coeff[i] = args[i + arg_base]; } @@ -131,7 +133,7 @@ command_sos_filter_set_state(uint32_t *args) // copy state data uint8_t section_idx = args[1]; validate_section_index(sf, section_idx); - const uint8_t arg_base = 2; + uint8_t arg_base = 2; sf->filter[section_idx].state[0] = args[0 + arg_base]; sf->filter[section_idx].state[1] = args[1 + arg_base]; } @@ -147,7 +149,7 @@ command_sos_filter_activate(uint32_t *args) if (n_sections > sf->max_sections) shutdown("Filter section index larger than max_sections"); sf->n_sections = n_sections; - const uint8_t coeff_int_bits = args[2]; + uint8_t coeff_int_bits = args[2]; sf->coeff_frac_bits = (31 - coeff_int_bits); sf->coeff_rounding = (1 << (sf->coeff_frac_bits - 1)); } diff --git a/src/sos_filter.h b/src/sos_filter.h index 6c215dda3..f235cb427 100644 --- a/src/sos_filter.h +++ b/src/sos_filter.h @@ -5,8 +5,7 @@ struct sos_filter; -int32_t sosfilt(struct sos_filter *sf - , const int32_t unfiltered_value); +int32_t sosfilt(struct sos_filter *sf, int32_t unfiltered_value); struct sos_filter *sos_filter_oid_lookup(uint8_t oid); #endif // sos_filter.h From 40242b2e3394dbc5964a13ed1e0b9798c327e65e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 16:29:45 -0500 Subject: [PATCH 067/108] sos_filter: Propagate overflow errors instead of a shutdown Pass an overflow error back to the caller instead of invoking a shutdown(). Signed-off-by: Kevin O'Connor --- src/sos_filter.c | 52 +++++++++++++++++++++++--------------------- src/sos_filter.h | 3 +-- src/trigger_analog.c | 9 ++++++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/sos_filter.c b/src/sos_filter.c index 96281e282..036c50ce5 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -9,17 +9,14 @@ #include "sched.h" // shutdown #include "sos_filter.h" // sos_filter -typedef int32_t fixedQ_coeff_t; -typedef int32_t fixedQ_value_t; - // filter strucutre sizes #define SECTION_WIDTH 5 #define STATE_WIDTH 2 struct sos_filter_section { // filter composed of second order sections - fixedQ_coeff_t coeff[SECTION_WIDTH]; // aka sos - fixedQ_value_t state[STATE_WIDTH]; // aka zi + int32_t coeff[SECTION_WIDTH]; // aka sos + int32_t state[STATE_WIDTH]; // aka zi }; struct sos_filter { @@ -29,15 +26,15 @@ struct sos_filter { struct sos_filter_section filter[0]; }; -static inline uint8_t +static inline int overflows_int32(int64_t value) { return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; } -// Multiply a coefficient in fixedQ_coeff_t by a value fixedQ_value_t -static inline fixedQ_value_t -fixed_mul(struct sos_filter *sf, fixedQ_coeff_t coeff, fixedQ_value_t value) +// Multiply a coeff*value and shift result by coeff_frac_bits +static inline int +fixed_mul(struct sos_filter *sf, int32_t coeff, int32_t value, int32_t *res) { // This optimizes to single cycle SMULL on Arm Coretex M0+ int64_t product = (int64_t)coeff * (int64_t)value; @@ -45,35 +42,40 @@ fixed_mul(struct sos_filter *sf, fixedQ_coeff_t coeff, fixedQ_value_t value) product += sf->coeff_rounding; // shift the decimal right to discard the coefficient fractional bits int64_t result = product >> sf->coeff_frac_bits; - // check for overflow of int32_t - if (overflows_int32(result)) { - shutdown("fixed_mul: overflow"); - } // truncate significant 32 bits - return (fixedQ_value_t)result; + *res = (int32_t)result; + // check for overflow of int32_t + if (overflows_int32(result)) + return -1; + return 0; } // Apply the sosfilt algorithm to a new datapoint -// returns the fixedQ_value_t filtered value -int32_t -sosfilt(struct sos_filter *sf, int32_t unfiltered_value) +int +sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { - fixedQ_value_t cur_val = unfiltered_value; + int32_t cur_val = *pvalue; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { struct sos_filter_section *section = &(sf->filter[section_idx]); // apply the section's filter coefficients to input - fixedQ_value_t next_val = fixed_mul(sf, section->coeff[0], cur_val); + int32_t next_val, c1_cur, c2_cur, c3_next, c4_next; + int ret = fixed_mul(sf, section->coeff[0], cur_val, &next_val); next_val += section->state[0]; - section->state[0] = fixed_mul(sf, section->coeff[1], cur_val) - - fixed_mul(sf, section->coeff[3], next_val) - + (section->state[1]); - section->state[1] = fixed_mul(sf, section->coeff[2], cur_val) - - fixed_mul(sf, section->coeff[4], next_val); + ret |= fixed_mul(sf, section->coeff[1], cur_val, &c1_cur); + ret |= fixed_mul(sf, section->coeff[3], next_val, &c3_next); + ret |= fixed_mul(sf, section->coeff[2], cur_val, &c2_cur); + ret |= fixed_mul(sf, section->coeff[4], next_val, &c4_next); + if (ret) + // Overflow + return -1; + section->state[0] = c1_cur - c3_next + section->state[1]; + section->state[1] = c2_cur - c4_next; cur_val = next_val; } - return (int32_t)cur_val; + *pvalue = cur_val; + return 0; } // Create an sos_filter diff --git a/src/sos_filter.h b/src/sos_filter.h index f235cb427..b697a2686 100644 --- a/src/sos_filter.h +++ b/src/sos_filter.h @@ -4,8 +4,7 @@ #include struct sos_filter; - -int32_t sosfilt(struct sos_filter *sf, int32_t unfiltered_value); +int sos_filter_apply(struct sos_filter *sf, int32_t *pvalue); struct sos_filter *sos_filter_oid_lookup(uint8_t oid); #endif // sos_filter.h diff --git a/src/trigger_analog.c b/src/trigger_analog.c index 9c0220e19..4a8419d3b 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -9,7 +9,7 @@ #include "board/misc.h" // timer_read_time #include "command.h" // DECL_COMMAND #include "sched.h" // shutdown -#include "sos_filter.h" // fixedQ12_t +#include "sos_filter.h" // sos_filter_apply #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger @@ -157,7 +157,12 @@ trigger_analog_update(struct trigger_analog *ta, const int32_t sample) } // perform filtering - const fixedQ16_t filtered_grams = sosfilt(ta->sf, (fixedQ16_t)raw_grams); + int32_t filtered_grams = raw_grams; + int ret = sos_filter_apply(ta->sf, &filtered_grams); + if (ret) { + trigger_error(ta, ERROR_OVERFLOW); + return; + } // update trigger state if (abs(filtered_grams) >= ta->trigger_grams_fixed) { From 64133997842991287e510c0a4d04e52465580618 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 18:30:09 -0500 Subject: [PATCH 068/108] sos_filter: No need to support "is_init" in MCU_SosFilter Rename the SosFilter class to MCU_SosFilter. Automatically reload the filter coefficients on a reset_filter() call, so there is no need to support loading of the filter at init time. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 5 +- klippy/extras/sos_filter.py | 101 +++++++++++-------------------- 2 files changed, 36 insertions(+), 70 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 9414ffc54..0ee7b77ea 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -215,8 +215,8 @@ class ContinuousTareFilterHelper: buzz_delay, notches, notch_quality) def _create_filter(self, fixed_filter, cmd_queue): - return sos_filter.SosFilter(self._sensor.get_mcu(), cmd_queue, - fixed_filter, 4) + return sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, + fixed_filter, 4) def update_from_command(self, gcmd, cq=None): gcmd_filter = self._build_filter(gcmd) @@ -323,7 +323,6 @@ class MCU_trigger_analog: self._printer.register_event_handler("klippy:connect", self._on_connect) def _config_commands(self): - self._sos_filter.create_filter() self._mcu.add_config_cmd( "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index cbd6c51c9..dd9725d06 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -141,7 +141,7 @@ class FixedPointSosFilter: # Control an `sos_filter` object on the MCU -class SosFilter: +class MCU_SosFilter: # fixed_point_filter should be an FixedPointSosFilter instance. A filter of # size 0 will create a passthrough filter. # max_sections should be the largest number of sections you expect @@ -154,81 +154,48 @@ class SosFilter: self._max_sections = max_sections if self._max_sections is None: self._max_sections = self._filter.get_num_sections() - self._cmd_set_section = [ - "sos_filter_set_section oid=%d section_idx=%d" - " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", - "sos_filter_set_section oid=%c section_idx=%c" - " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i"] - self._cmd_config_state = [ - "sos_filter_set_state oid=%d section_idx=%d state0=%i state1=%i", - "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i"] - self._cmd_activate = [ - "sos_filter_set_active oid=%d n_sections=%d coeff_int_bits=%d", - "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c"] + self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None + self._last_sent_coeffs = [None] * self._max_sections + self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" + % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) def _build_config(self): - cmds = [self._cmd_set_section, self._cmd_config_state, - self._cmd_activate] - for cmd in cmds: - cmd.append(self._mcu.lookup_command(cmd[1], cq=self._cmd_queue)) + self._set_section_cmd = self._mcu.lookup_command( + "sos_filter_set_section oid=%c section_idx=%c" + " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", cq=self._cmd_queue) + self._set_state_cmd = self._mcu.lookup_command( + "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", + cq=self._cmd_queue) + self._set_active_cmd = self._mcu.lookup_command( + "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", + cq=self._cmd_queue) def get_oid(self): return self._oid - # create an uninitialized filter object - def create_filter(self): - self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" - % (self._oid, self._max_sections)) - self._configure_filter(is_init=True) + # Change the filter coefficients and state at runtime + # fixed_point_filter should be an FixedPointSosFilter instance + def change_filter(self, fixed_point_filter): + self._filter = fixed_point_filter - # either setup an init command or send the command based on a flag - def _cmd(self, command, args, is_init=False): - if is_init: - self._mcu.add_config_cmd(command[0] % args, is_init=True) - else: - command[2].send(args) - - def _set_filter_sections(self, is_init=False): - for i, section in enumerate(self._filter.get_filter_sections()): - args = (self._oid, i, section[0], section[1], section[2], - section[3], section[4]) - self._cmd(self._cmd_set_section, args, is_init) - - def _set_filter_state(self, is_init=False): - for i, state in enumerate(self._filter.get_initial_state()): - args = (self._oid, i, state[0], state[1]) - self._cmd(self._cmd_config_state, args, is_init) - - def _activate_filter(self, is_init=False): - args = (self._oid, self._filter.get_num_sections(), - self._filter.get_coeff_int_bits()) - self._cmd(self._cmd_activate, args, is_init) - - # configure the filter sections on the mcu - # filters should be an array of filter sections in SciPi SOS format - # sos_filter_state should be an array of zi filter state elements - def _configure_filter(self, is_init=False): + # Resets the filter state back to initial conditions at runtime + def reset_filter(self): num_sections = self._filter.get_num_sections() if num_sections > self._max_sections: raise ValueError("Too many filter sections: %i, The max is %i" % (num_sections, self._max_sections,)) - # convert to fixed point to find errors - # no errors, state is accepted - # configure MCU filter and activate - self._set_filter_sections(is_init) - self._set_filter_state(is_init,) - self._activate_filter(is_init) - - # Change the filter coefficients and state at runtime - # fixed_point_filter should be an FixedPointSosFilter instance - # cq is an optional command queue to for command sequencing - def change_filter(self, fixed_point_filter): - self._filter = fixed_point_filter - self._configure_filter(False) - - # Resets the filter state back to initial conditions at runtime - # cq is an optional command queue to for command sequencing - def reset_filter(self): - self._set_filter_state(False) - self._activate_filter(False) + # Send section coefficients (if they have changed) + for i, section in enumerate(self._filter.get_filter_sections()): + args = (self._oid, i, section[0], section[1], section[2], + section[3], section[4]) + if args == self._last_sent_coeffs[i]: + continue + self._set_section_cmd.send(args) + self._last_sent_coeffs[i] = args + # Send section initial states + for i, state in enumerate(self._filter.get_initial_state()): + self._set_state_cmd.send([self._oid, i, state[0], state[1]]) + # Activate filter + self._set_active_cmd.send([self._oid, num_sections, + self._filter.get_coeff_int_bits()]) From 3b5045ed9e0217c93ca984d56d866c2826c564c6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 19:44:15 -0500 Subject: [PATCH 069/108] sos_filter: Handle fixed point conversion within MCU_SosFilter Merge the logic from the FixedPointSosFilter class into the MCU_SosFilter class. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 18 ++--- klippy/extras/sos_filter.py | 131 +++++++++++++------------------ 2 files changed, 61 insertions(+), 88 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 0ee7b77ea..3b08b4510 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -163,13 +163,9 @@ class ContinuousTareFilter: # create a filter design from the parameters def design_filter(self, error_func): - design = sos_filter.DigitalFilter(self.sps, error_func, self.drift, + return sos_filter.DigitalFilter(self.sps, error_func, self.drift, self.drift_delay, self.buzz, self.buzz_delay, self.notches, self.notch_quality) - fixed_filter = sos_filter.FixedPointSosFilter( - design.get_filter_sections(), design.get_initial_state(), - Q2_INT_BITS, Q16_INT_BITS) - return fixed_filter # Combine ContinuousTareFilter and SosFilter into an easy-to-use class @@ -214,9 +210,11 @@ class ContinuousTareFilterHelper: return ContinuousTareFilter(self._sps, drift, drift_delay, buzz, buzz_delay, notches, notch_quality) - def _create_filter(self, fixed_filter, cmd_queue): - return sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, - fixed_filter, 4) + def _create_filter(self, design, cmd_queue): + sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, + Q2_INT_BITS, Q16_INT_BITS) + sf.set_filter_design(design) + return sf def update_from_command(self, gcmd, cq=None): gcmd_filter = self._build_filter(gcmd) @@ -224,8 +222,8 @@ class ContinuousTareFilterHelper: if self._active_design == gcmd_filter: return # update MCU filter from GCode command - self._sos_filter.change_filter( - self._active_design.design_filter(gcmd.error)) + design = self._active_design.design_filter(gcmd.error) + self._sos_filter.set_filter_design(design) def get_sos_filter(self): return self._sos_filter diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index dd9725d06..9c9cda499 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -63,41 +63,25 @@ class DigitalFilter: def get_initial_state(self): return self.initial_state -# container that accepts SciPy formatted SOS filter data and converts it to a -# selected fixed point representation. This data could come from DigitalFilter, -# static data, config etc. -class FixedPointSosFilter: - # filter_sections is an array of SciPy formatted SOS filter sections (sos) - # initial_state is an array of SciPy formatted SOS state sections (zi) - def __init__(self, filter_sections=None, initial_state=None, - coeff_int_bits=2, value_int_bits=15): - filter_sections = [] if filter_sections is None else filter_sections - initial_state = [] if initial_state is None else initial_state - num_sections = len(filter_sections) - num_state = len(initial_state) - if num_state != num_sections: - raise ValueError("The number of filter sections (%i) and state " - "sections (%i) must be equal" % ( - num_sections, num_state)) - self._coeff_int_bits = self._validate_int_bits(coeff_int_bits) - self._value_int_bits = self._validate_int_bits(value_int_bits) - self._filter = self._convert_filter(filter_sections) - self._state = self._convert_state(initial_state) - def get_filter_sections(self): - return self._filter - - def get_initial_state(self): - return self._state - - def get_coeff_int_bits(self): - return self._coeff_int_bits - - def get_value_int_bits(self): - return self._value_int_bits - - def get_num_sections(self): - return len(self._filter) +# Control an `sos_filter` object on the MCU +class MCU_SosFilter: + # max_sections should be the largest number of sections you expect + # to use at runtime. + def __init__(self, mcu, cmd_queue, max_sections, + coeff_int_bits=2, value_int_bits=15): + self._mcu = mcu + self._cmd_queue = cmd_queue + self._oid = self._mcu.create_oid() + self._max_sections = max_sections + self._coeff_int_bits = coeff_int_bits + self._value_int_bits = value_int_bits + self._design = None + self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None + self._last_sent_coeffs = [None] * self._max_sections + self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" + % (self._oid, self._max_sections)) + self._mcu.register_config_callback(self._build_config) def _validate_int_bits(self, int_bits): if int_bits < 1 or int_bits > 30: @@ -105,8 +89,25 @@ class FixedPointSosFilter: " value between 1 and 30" % (int_bits,)) return int_bits + def _build_config(self): + self._set_section_cmd = self._mcu.lookup_command( + "sos_filter_set_section oid=%c section_idx=%c" + " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", cq=self._cmd_queue) + self._set_state_cmd = self._mcu.lookup_command( + "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", + cq=self._cmd_queue) + self._set_active_cmd = self._mcu.lookup_command( + "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", + cq=self._cmd_queue) + + def get_oid(self): + return self._oid + # convert the SciPi SOS filters to fixed point format - def _convert_filter(self, filter_sections): + def _convert_filter(self): + if self._design is None: + return [] + filter_sections = self._design.get_filter_sections() sos_fixed = [] for section in filter_sections: nun_coeff = len(section) @@ -125,7 +126,10 @@ class FixedPointSosFilter: return sos_fixed # convert the SOS filter state matrix (zi) to fixed point format - def _convert_state(self, filter_state): + def _convert_state(self): + if self._design is None: + return [] + filter_state = self._design.get_initial_state() sos_state = [] for section in filter_state: nun_states = len(section) @@ -139,54 +143,25 @@ class FixedPointSosFilter: sos_state.append(fixed_state) return sos_state - -# Control an `sos_filter` object on the MCU -class MCU_SosFilter: - # fixed_point_filter should be an FixedPointSosFilter instance. A filter of - # size 0 will create a passthrough filter. - # max_sections should be the largest number of sections you expect - # to use at runtime. The default is the size of the fixed_point_filter. - def __init__(self, mcu, cmd_queue, fixed_point_filter, max_sections=None): - self._mcu = mcu - self._cmd_queue = cmd_queue - self._oid = self._mcu.create_oid() - self._filter = fixed_point_filter - self._max_sections = max_sections - if self._max_sections is None: - self._max_sections = self._filter.get_num_sections() - self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None - self._last_sent_coeffs = [None] * self._max_sections - self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" - % (self._oid, self._max_sections)) - self._mcu.register_config_callback(self._build_config) - - def _build_config(self): - self._set_section_cmd = self._mcu.lookup_command( - "sos_filter_set_section oid=%c section_idx=%c" - " sos0=%i sos1=%i sos2=%i sos3=%i sos4=%i", cq=self._cmd_queue) - self._set_state_cmd = self._mcu.lookup_command( - "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", - cq=self._cmd_queue) - self._set_active_cmd = self._mcu.lookup_command( - "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", - cq=self._cmd_queue) - - def get_oid(self): - return self._oid - # Change the filter coefficients and state at runtime - # fixed_point_filter should be an FixedPointSosFilter instance - def change_filter(self, fixed_point_filter): - self._filter = fixed_point_filter + def set_filter_design(self, design): + self._design = design # Resets the filter state back to initial conditions at runtime def reset_filter(self): - num_sections = self._filter.get_num_sections() + # Generate filter parameters + sos_fixed = self._convert_filter() + sos_state = self._convert_state() + num_sections = len(sos_fixed) if num_sections > self._max_sections: raise ValueError("Too many filter sections: %i, The max is %i" % (num_sections, self._max_sections,)) + if len(sos_state) != num_sections: + raise ValueError("The number of filter sections (%i) and state " + "sections (%i) must be equal" + % (num_sections, len(sos_state))) # Send section coefficients (if they have changed) - for i, section in enumerate(self._filter.get_filter_sections()): + for i, section in enumerate(sos_fixed): args = (self._oid, i, section[0], section[1], section[2], section[3], section[4]) if args == self._last_sent_coeffs[i]: @@ -194,8 +169,8 @@ class MCU_SosFilter: self._set_section_cmd.send(args) self._last_sent_coeffs[i] = args # Send section initial states - for i, state in enumerate(self._filter.get_initial_state()): + for i, state in enumerate(sos_state): self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Activate filter self._set_active_cmd.send([self._oid, num_sections, - self._filter.get_coeff_int_bits()]) + self._coeff_int_bits]) From 109f13c7977381dc0ce926e5842621260a5d2e1b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 10 Jan 2026 19:17:05 -0500 Subject: [PATCH 070/108] sos_filter: Consistently use "frac_bits" instead of "int_bits" Internally describe the Qx.y format using the number of fractional bits. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 18 +++++++--------- klippy/extras/sos_filter.py | 37 ++++++++++++++++---------------- src/sos_filter.c | 34 ++++++++++++++--------------- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 3b08b4510..56992e9f8 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -10,10 +10,8 @@ from . import probe, sos_filter, load_cell, hx71x, ads1220 np = None # delay NumPy import until configuration time # constants for fixed point numbers -Q2_INT_BITS = 2 -Q2_FRAC_BITS = (32 - (1 + Q2_INT_BITS)) -Q16_INT_BITS = 16 -Q16_FRAC_BITS = (32 - (1 + Q16_INT_BITS)) +Q2_29_FRAC_BITS = 29 +Q16_15_FRAC_BITS = 15 class TapAnalysis: @@ -212,7 +210,7 @@ class ContinuousTareFilterHelper: def _create_filter(self, design, cmd_queue): sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_INT_BITS, Q16_INT_BITS) + Q2_29_FRAC_BITS, Q16_15_FRAC_BITS) sf.set_filter_design(design) return sf @@ -282,15 +280,15 @@ class LoadCellProbeConfigHelper: raise cmd_err("Load cell force_safety_limit exceeds sensor range!") return safety_min, safety_max - # calculate 1/counts_per_gram in Q2 fixed point + # calculate 1/counts_per_gram in Q2.29 fixed point def get_grams_per_count(self): counts_per_gram = self._load_cell.get_counts_per_gram() # The counts_per_gram could be so large that it becomes 0.0 when - # converted to Q2 format. This would mean the ADC range only measures a - # few grams which seems very unlikely. Treat this as an error: - if counts_per_gram >= 2**Q2_FRAC_BITS: + # converted to Q2.29 format. This would mean the ADC range only measures + # a few grams which seems very unlikely. Treat this as an error: + if counts_per_gram >= 2**Q2_29_FRAC_BITS: raise OverflowError("counts_per_gram value is too large to filter") - return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_INT_BITS) + return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_29_FRAC_BITS) # MCU_trigger_analog is the interface to `trigger_analog` on the MCU diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 9c9cda499..021dfff28 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -6,18 +6,17 @@ MAX_INT32 = (2 ** 31) MIN_INT32 = -(2 ** 31) - 1 -def assert_is_int32(value, error): +def assert_is_int32(value, frac_bits): if value > MAX_INT32 or value < MIN_INT32: - raise OverflowError(error) + raise OverflowError("Fixed point Q%d.%d overflow" + % (31-frac_bits, frac_bits)) return value # convert a floating point value to a 32 bit fixed point representation # checks for overflow -def to_fixed_32(value, int_bits): - fractional_bits = (32 - (1 + int_bits)) - fixed_val = int(value * (2 ** fractional_bits)) - return assert_is_int32(fixed_val, "Fixed point Q%i overflow" - % (int_bits,)) +def to_fixed_32(value, frac_bits): + fixed_val = int(value * (2**frac_bits)) + return assert_is_int32(fixed_val, frac_bits) # Digital filter designer and container @@ -69,13 +68,13 @@ class MCU_SosFilter: # max_sections should be the largest number of sections you expect # to use at runtime. def __init__(self, mcu, cmd_queue, max_sections, - coeff_int_bits=2, value_int_bits=15): + coeff_frac_bits=29, value_frac_bits=16): self._mcu = mcu self._cmd_queue = cmd_queue self._oid = self._mcu.create_oid() self._max_sections = max_sections - self._coeff_int_bits = coeff_int_bits - self._value_int_bits = value_int_bits + self._coeff_frac_bits = coeff_frac_bits + self._value_frac_bits = value_frac_bits self._design = None self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None self._last_sent_coeffs = [None] * self._max_sections @@ -83,11 +82,11 @@ class MCU_SosFilter: % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) - def _validate_int_bits(self, int_bits): - if int_bits < 1 or int_bits > 30: - raise ValueError("The number of integer bits (%i) must be a" - " value between 1 and 30" % (int_bits,)) - return int_bits + def _validate_frac_bits(self, frac_bits): + if frac_bits < 0 or frac_bits > 31: + raise ValueError("The number of fractional bits (%i) must be a" + " value between 0 and 31" % (frac_bits,)) + return frac_bits def _build_config(self): self._set_section_cmd = self._mcu.lookup_command( @@ -97,7 +96,7 @@ class MCU_SosFilter: "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", cq=self._cmd_queue) self._set_active_cmd = self._mcu.lookup_command( - "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c", + "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c", cq=self._cmd_queue) def get_oid(self): @@ -117,7 +116,7 @@ class MCU_SosFilter: fixed_section = [] for col, coeff in enumerate(section): if col != 3: # omit column 3 - fixed_coeff = to_fixed_32(coeff, self._coeff_int_bits) + fixed_coeff = to_fixed_32(coeff, self._coeff_frac_bits) fixed_section.append(fixed_coeff) elif coeff != 1.0: # double check column 3 is always 1.0 raise ValueError("Coefficient 3 is expected to be 1.0" @@ -139,7 +138,7 @@ class MCU_SosFilter: % (nun_states,)) fixed_state = [] for col, value in enumerate(section): - fixed_state.append(to_fixed_32(value, self._value_int_bits)) + fixed_state.append(to_fixed_32(value, self._value_frac_bits)) sos_state.append(fixed_state) return sos_state @@ -173,4 +172,4 @@ class MCU_SosFilter: self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Activate filter self._set_active_cmd.send([self._oid, num_sections, - self._coeff_int_bits]) + self._coeff_frac_bits]) diff --git a/src/sos_filter.c b/src/sos_filter.c index 036c50ce5..60c6abf4c 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -21,7 +21,6 @@ struct sos_filter_section { struct sos_filter { uint8_t max_sections, n_sections, coeff_frac_bits; - uint32_t coeff_rounding; // filter composed of second order sections struct sos_filter_section filter[0]; }; @@ -33,15 +32,17 @@ overflows_int32(int64_t value) } // Multiply a coeff*value and shift result by coeff_frac_bits -static inline int -fixed_mul(struct sos_filter *sf, int32_t coeff, int32_t value, int32_t *res) +static int +fixed_mul(int32_t coeff, int32_t value, uint_fast8_t frac_bits, int32_t *res) { // This optimizes to single cycle SMULL on Arm Coretex M0+ - int64_t product = (int64_t)coeff * (int64_t)value; - // round up at the last bit to be shifted away - product += sf->coeff_rounding; - // shift the decimal right to discard the coefficient fractional bits - int64_t result = product >> sf->coeff_frac_bits; + int64_t result = (int64_t)coeff * (int64_t)value; + if (frac_bits) { + // round up at the last bit to be shifted away + result += 1 << (frac_bits - 1); + // shift the decimal right to discard the coefficient fractional bits + result >>= frac_bits; + } // truncate significant 32 bits *res = (int32_t)result; // check for overflow of int32_t @@ -55,17 +56,18 @@ int sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { int32_t cur_val = *pvalue; + uint_fast8_t cfb = sf->coeff_frac_bits; // foreach section for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { struct sos_filter_section *section = &(sf->filter[section_idx]); // apply the section's filter coefficients to input int32_t next_val, c1_cur, c2_cur, c3_next, c4_next; - int ret = fixed_mul(sf, section->coeff[0], cur_val, &next_val); + int ret = fixed_mul(section->coeff[0], cur_val, cfb, &next_val); next_val += section->state[0]; - ret |= fixed_mul(sf, section->coeff[1], cur_val, &c1_cur); - ret |= fixed_mul(sf, section->coeff[3], next_val, &c3_next); - ret |= fixed_mul(sf, section->coeff[2], cur_val, &c2_cur); - ret |= fixed_mul(sf, section->coeff[4], next_val, &c4_next); + ret |= fixed_mul(section->coeff[1], cur_val, cfb, &c1_cur); + ret |= fixed_mul(section->coeff[3], next_val, cfb, &c3_next); + ret |= fixed_mul(section->coeff[2], cur_val, cfb, &c2_cur); + ret |= fixed_mul(section->coeff[4], next_val, cfb, &c4_next); if (ret) // Overflow return -1; @@ -151,9 +153,7 @@ command_sos_filter_activate(uint32_t *args) if (n_sections > sf->max_sections) shutdown("Filter section index larger than max_sections"); sf->n_sections = n_sections; - uint8_t coeff_int_bits = args[2]; - sf->coeff_frac_bits = (31 - coeff_int_bits); - sf->coeff_rounding = (1 << (sf->coeff_frac_bits - 1)); + sf->coeff_frac_bits = args[2] & 0x3f; } DECL_COMMAND(command_sos_filter_activate - , "sos_filter_set_active oid=%c n_sections=%c coeff_int_bits=%c"); + , "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c"); From 7ec82baca3f37bf3193c9316e538b538d8075e6f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 8 Jan 2026 14:01:18 -0500 Subject: [PATCH 071/108] sos_filter: Move offset/scale support from trigger_analog.c to sos_filter.c Support offsetting and scaling the initial raw value prior to processing in the sos_filter. Remove that support from trigger_analog.c . Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 22 ++++--- klippy/extras/sos_filter.py | 28 +++++++-- src/sos_filter.c | 29 ++++++++- src/trigger_analog.c | 103 +++---------------------------- 4 files changed, 72 insertions(+), 110 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 56992e9f8..2d5fce81e 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -210,7 +210,7 @@ class ContinuousTareFilterHelper: def _create_filter(self, design, cmd_queue): sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_29_FRAC_BITS, Q16_15_FRAC_BITS) + Q2_29_FRAC_BITS) sf.set_filter_design(design) return sf @@ -280,7 +280,7 @@ class LoadCellProbeConfigHelper: raise cmd_err("Load cell force_safety_limit exceeds sensor range!") return safety_min, safety_max - # calculate 1/counts_per_gram in Q2.29 fixed point + # calculate 1/counts_per_gram def get_grams_per_count(self): counts_per_gram = self._load_cell.get_counts_per_gram() # The counts_per_gram could be so large that it becomes 0.0 when @@ -288,7 +288,7 @@ class LoadCellProbeConfigHelper: # a few grams which seems very unlikely. Treat this as an error: if counts_per_gram >= 2**Q2_29_FRAC_BITS: raise OverflowError("counts_per_gram value is too large to filter") - return sos_filter.to_fixed_32((1. / counts_per_gram), Q2_29_FRAC_BITS) + return 1. / counts_per_gram # MCU_trigger_analog is the interface to `trigger_analog` on the MCU @@ -330,9 +330,8 @@ class MCU_trigger_analog: "trigger_analog_state oid=%c is_homing_trigger=%c " "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" - " trigger_grams=%u grams_per_count=%i", cq=self._cmd_queue) + "trigger_analog_set_range oid=%c safety_counts_min=%i" + " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" " error_reason=%c clock=%u rest_ticks=%u timeout=%u", @@ -359,10 +358,13 @@ class MCU_trigger_analog: self._load_cell.tare(tare_counts) # update internal tare value safety_min, safety_max = self._config_helper.get_safety_range(gcmd) - args = [self._oid, safety_min, safety_max, int(tare_counts), - self._config_helper.get_trigger_force_grams(gcmd), - self._config_helper.get_grams_per_count()] - self._set_range_cmd.send(args) + trigger_val = self._config_helper.get_trigger_force_grams(gcmd) + tval32 = sos_filter.to_fixed_32(trigger_val, Q16_15_FRAC_BITS) + self._set_range_cmd.send([self._oid, safety_min, safety_max, tval32]) + gpc = self._config_helper.get_grams_per_count() + Q17_14_FRAC_BITS = 14 + self._sos_filter.set_offset_scale(int(-tare_counts), gpc, + Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) self._sos_filter.reset_filter() def home_start(self, print_time): diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 021dfff28..6456eaa5c 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -67,17 +67,20 @@ class DigitalFilter: class MCU_SosFilter: # max_sections should be the largest number of sections you expect # to use at runtime. - def __init__(self, mcu, cmd_queue, max_sections, - coeff_frac_bits=29, value_frac_bits=16): + def __init__(self, mcu, cmd_queue, max_sections, coeff_frac_bits=29): self._mcu = mcu self._cmd_queue = cmd_queue self._oid = self._mcu.create_oid() self._max_sections = max_sections self._coeff_frac_bits = coeff_frac_bits - self._value_frac_bits = value_frac_bits + self._value_frac_bits = self._scale_frac_bits = 0 self._design = None - self._set_section_cmd = self._set_state_cmd = self._set_active_cmd =None + self._offset = 0 + self._scale = 1 + self._set_section_cmd = self._set_state_cmd = None + self._set_active_cmd = self._set_offset_scale_cmd = None self._last_sent_coeffs = [None] * self._max_sections + self._last_sent_offset_scale = None self._mcu.add_config_cmd("config_sos_filter oid=%d max_sections=%d" % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) @@ -95,6 +98,9 @@ class MCU_SosFilter: self._set_state_cmd = self._mcu.lookup_command( "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i", cq=self._cmd_queue) + self._set_offset_scale_cmd = self._mcu.lookup_command( + "sos_filter_set_offset_scale oid=%c offset=%i" + " scale=%i scale_frac_bits=%c", cq=self._cmd_queue) self._set_active_cmd = self._mcu.lookup_command( "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c", cq=self._cmd_queue) @@ -142,6 +148,15 @@ class MCU_SosFilter: sos_state.append(fixed_state) return sos_state + # Set conversion of a raw value 1 to a 1.0 value processed by sos filter + def set_offset_scale(self, offset, scale, scale_frac_bits=0, + value_frac_bits=0): + self._offset = offset + self._value_frac_bits = value_frac_bits + scale_mult = scale * float(1 << value_frac_bits) + self._scale = to_fixed_32(scale_mult, scale_frac_bits) + self._scale_frac_bits = scale_frac_bits + # Change the filter coefficients and state at runtime def set_filter_design(self, design): self._design = design @@ -170,6 +185,11 @@ class MCU_SosFilter: # Send section initial states for i, state in enumerate(sos_state): self._set_state_cmd.send([self._oid, i, state[0], state[1]]) + # Send offset/scale (if they have changed) + args = (self._oid, self._offset, self._scale, self._scale_frac_bits) + if args != self._last_sent_offset_scale: + self._set_offset_scale_cmd.send(args) + self._last_sent_offset_scale = args # Activate filter self._set_active_cmd.send([self._oid, num_sections, self._coeff_frac_bits]) diff --git a/src/sos_filter.c b/src/sos_filter.c index 60c6abf4c..c11ae3df8 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -20,7 +20,8 @@ struct sos_filter_section { }; struct sos_filter { - uint8_t max_sections, n_sections, coeff_frac_bits; + uint8_t max_sections, n_sections, coeff_frac_bits, scale_frac_bits; + int32_t offset, scale; // filter composed of second order sections struct sos_filter_section filter[0]; }; @@ -55,9 +56,19 @@ fixed_mul(int32_t coeff, int32_t value, uint_fast8_t frac_bits, int32_t *res) int sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { - int32_t cur_val = *pvalue; - uint_fast8_t cfb = sf->coeff_frac_bits; + int32_t raw_val = *pvalue; + + // Apply offset and scale + int32_t offset = sf->offset, offset_val = raw_val + offset, cur_val; + if ((offset >= 0) != (offset_val >= raw_val)) + // Overflow + return -1; + int ret = fixed_mul(sf->scale, offset_val, sf->scale_frac_bits, &cur_val); + if (ret) + return -1; + // foreach section + uint_fast8_t cfb = sf->coeff_frac_bits; for (int section_idx = 0; section_idx < sf->n_sections; section_idx++) { struct sos_filter_section *section = &(sf->filter[section_idx]); // apply the section's filter coefficients to input @@ -144,6 +155,18 @@ command_sos_filter_set_state(uint32_t *args) DECL_COMMAND(command_sos_filter_set_state , "sos_filter_set_state oid=%c section_idx=%c state0=%i state1=%i"); +// Set incoming sample offset/scaling +void +command_trigger_analog_set_offset_scale(uint32_t *args) +{ + struct sos_filter *sf = sos_filter_oid_lookup(args[0]); + sf->offset = args[1]; + sf->scale = args[2]; + sf->scale_frac_bits = args[3] & 0x3f; +} +DECL_COMMAND(command_trigger_analog_set_offset_scale, + "sos_filter_set_offset_scale oid=%c offset=%i scale=%i scale_frac_bits=%c"); + // Set one section of the filter void command_sos_filter_activate(uint32_t *args) diff --git a/src/trigger_analog.c b/src/trigger_analog.c index 4a8419d3b..df64a4f40 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -13,25 +13,6 @@ #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger -// Q2.29 -typedef int32_t fixedQ2_t; -#define FIXEDQ2 2 -#define FIXEDQ2_FRAC_BITS ((32 - FIXEDQ2) - 1) - -// Q32.29 - a Q2.29 value stored in int64 -typedef int64_t fixedQ32_t; -#define FIXEDQ32_FRAC_BITS FIXEDQ2_FRAC_BITS - -// Q16.15 -typedef int32_t fixedQ16_t; -#define FIXEDQ16 16 -#define FIXEDQ16_FRAC_BITS ((32 - FIXEDQ16) - 1) - -// Q48.15 - a Q16.15 value stored in int64 -typedef int64_t fixedQ48_t; -#define FIXEDQ48_FRAC_BITS FIXEDQ16_FRAC_BITS - -#define MAX_TRIGGER_GRAMS ((1L << FIXEDQ16) - 1) #define ERROR_SAFETY_RANGE 0 #define ERROR_OVERFLOW 1 #define ERROR_WATCHDOG 2 @@ -45,42 +26,15 @@ enum {FLAG_IS_HOMING = 1 << 0 // Endstop Structure struct trigger_analog { struct timer time; - uint32_t trigger_grams, trigger_ticks, last_sample_ticks, rest_ticks; + uint32_t trigger_ticks, last_sample_ticks, rest_ticks; uint32_t homing_start_time; struct trsync *ts; - int32_t safety_counts_min, safety_counts_max, tare_counts; + int32_t safety_counts_min, safety_counts_max; uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; - fixedQ16_t trigger_grams_fixed; - fixedQ2_t grams_per_count; + int32_t trigger_value; struct sos_filter *sf; }; -static inline uint8_t -overflows_int32(int64_t value) { - return value > (int64_t)INT32_MAX || value < (int64_t)INT32_MIN; -} - -// returns the integer part of a fixedQ48_t -static inline int64_t -round_fixedQ48(const int64_t fixed_value) { - return fixed_value >> FIXEDQ48_FRAC_BITS; -} - -// Convert sensor counts to grams -static inline fixedQ48_t -counts_to_grams(struct trigger_analog *ta, const int32_t counts) { - // tearing ensures readings are referenced to 0.0g - const int32_t delta = counts - ta->tare_counts; - // convert sensor counts to grams by multiplication: 124 * 0.051 = 6.324 - // this optimizes to single cycle SMULL instruction - const fixedQ32_t product = (int64_t)delta * (int64_t)ta->grams_per_count; - // after multiplication there are 30 fraction bits, reduce to 15 - // caller verifies this wont overflow a 32bit int when truncated - const fixedQ48_t grams = product >> - (FIXEDQ32_FRAC_BITS - FIXEDQ48_FRAC_BITS); - return grams; -} - static inline uint8_t is_flag_set(const uint8_t mask, struct trigger_analog *ta) { @@ -149,23 +103,16 @@ trigger_analog_update(struct trigger_analog *ta, const int32_t sample) return; } - // convert sample to grams - const fixedQ48_t raw_grams = counts_to_grams(ta, sample); - if (overflows_int32(raw_grams)) { - trigger_error(ta, ERROR_OVERFLOW); - return; - } - // perform filtering - int32_t filtered_grams = raw_grams; - int ret = sos_filter_apply(ta->sf, &filtered_grams); + int32_t filtered_value = sample; + int ret = sos_filter_apply(ta->sf, &filtered_value); if (ret) { trigger_error(ta, ERROR_OVERFLOW); return; } // update trigger state - if (abs(filtered_grams) >= ta->trigger_grams_fixed) { + if (abs(filtered_value) >= ta->trigger_value) { try_trigger(ta, ta->last_sample_ticks); } } @@ -192,43 +139,13 @@ watchdog_event(struct timer *t) return SF_RESCHEDULE; } -static void -set_endstop_range(struct trigger_analog *ta - , int32_t safety_counts_min, int32_t safety_counts_max - , int32_t tare_counts, uint32_t trigger_grams - , fixedQ2_t grams_per_count) -{ - if (!(safety_counts_max >= safety_counts_min)) { - shutdown("Safety range reversed"); - } - if (trigger_grams > MAX_TRIGGER_GRAMS) { - shutdown("trigger_grams too large"); - } - // grams_per_count must be a positive fraction in Q2 format - const fixedQ2_t one = 1L << FIXEDQ2_FRAC_BITS; - if (grams_per_count < 0 || grams_per_count >= one) { - shutdown("grams_per_count is invalid"); - } - ta->safety_counts_min = safety_counts_min; - ta->safety_counts_max = safety_counts_max; - ta->tare_counts = tare_counts; - ta->trigger_grams = trigger_grams; - ta->trigger_grams_fixed = trigger_grams << FIXEDQ16_FRAC_BITS; - ta->grams_per_count = grams_per_count; -} - // Create a trigger_analog void command_config_trigger_analog(uint32_t *args) { struct trigger_analog *ta = oid_alloc( args[0], command_config_trigger_analog, sizeof(*ta)); - ta->flags = 0; - ta->trigger_ticks = 0; - ta->watchdog_max = 0; - ta->watchdog_count = 0; ta->sf = sos_filter_oid_lookup(args[1]); - set_endstop_range(ta, 0, 0, 0, 0, 0); } DECL_COMMAND(command_config_trigger_analog , "config_trigger_analog oid=%c sos_filter_oid=%c"); @@ -245,12 +162,12 @@ void command_trigger_analog_set_range(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - set_endstop_range(ta, args[1], args[2], args[3], args[4] - , (fixedQ16_t)args[5]); + ta->safety_counts_min = args[1]; + ta->safety_counts_max = args[2]; + ta->trigger_value = args[3]; } DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i tare_counts=%i" - " trigger_grams=%u grams_per_count=%i"); + " oid=%c safety_counts_min=%i safety_counts_max=%i trigger_value=%i"); // Home an axis void From 067539e0a394f99c2a874a5815f6dcb9d106017f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 12 Jan 2026 13:14:38 -0500 Subject: [PATCH 072/108] load_cell_probe: Move set_endstop_range() code to LoadCellProbingMove Move this load cell specific code from MCU_trigger_analog class to LoadCellProbingMove class. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 62 ++++++++++++++++++++------------ klippy/extras/sos_filter.py | 3 ++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 2d5fce81e..4daeec2a2 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -299,18 +299,20 @@ class MCU_trigger_analog: ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - def __init__(self, config, load_cell_inst, sos_filter_inst, config_helper, + def __init__(self, config, sensor_inst, sos_filter_inst, config_helper, trigger_dispatch): self._printer = config.get_printer() - self._load_cell = load_cell_inst self._sos_filter = sos_filter_inst self._config_helper = config_helper - self._sensor = load_cell_inst.get_sensor() + self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() # configure MCU objects self._dispatch = trigger_dispatch self._cmd_queue = self._dispatch.get_command_queue() self._oid = self._mcu.create_oid() + self._raw_min = self._raw_max = 0 + self._last_range_args = None + self._trigger_value = 0. self._config_commands() self._home_cmd = None self._query_cmd = None @@ -347,24 +349,27 @@ class MCU_trigger_analog: def get_mcu(self): return self._mcu - def get_load_cell(self): - return self._load_cell + def get_sos_filter(self): + return self._sos_filter def get_dispatch(self): return self._dispatch - def set_endstop_range(self, tare_counts, gcmd=None): - # update the load cell so it reflects the new tare value - self._load_cell.tare(tare_counts) - # update internal tare value - safety_min, safety_max = self._config_helper.get_safety_range(gcmd) - trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - tval32 = sos_filter.to_fixed_32(trigger_val, Q16_15_FRAC_BITS) - self._set_range_cmd.send([self._oid, safety_min, safety_max, tval32]) - gpc = self._config_helper.get_grams_per_count() - Q17_14_FRAC_BITS = 14 - self._sos_filter.set_offset_scale(int(-tare_counts), gpc, - Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) + def set_trigger_value(self, trigger_value): + self._trigger_value = trigger_value + + def set_raw_range(self, raw_min, raw_max): + self._raw_min = raw_min + self._raw_max = raw_max + + def reset_filter(self): + # Update parameters in mcu (if they have changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._raw_min, self._raw_max, tval32] + if args != self._last_range_args: + self._set_range_cmd.send(args) + self._last_range_args = args + # Update sos filter in mcu self._sos_filter.reset_filter() def home_start(self, print_time): @@ -397,15 +402,15 @@ class LoadCellProbingMove: "waiting for sensor data" } - def __init__(self, config, mcu_trigger_analog, param_helper, + def __init__(self, config, load_cell_inst, mcu_trigger_analog, param_helper, continuous_tare_filter_helper, config_helper): self._printer = config.get_printer() + self._load_cell = load_cell_inst self._mcu_trigger_analog = mcu_trigger_analog self._param_helper = param_helper self._continuous_tare_filter_helper = continuous_tare_filter_helper self._config_helper = config_helper self._mcu = mcu_trigger_analog.get_mcu() - self._load_cell = mcu_trigger_analog.get_load_cell() self._z_min_position = probe.lookup_minimum_z(config) self._dispatch = mcu_trigger_analog.get_dispatch() probe.LookupZSteppers(config, self._dispatch.add_stepper) @@ -433,7 +438,20 @@ class LoadCellProbingMove: tare_counts = np.average(np.array(tare_samples)[:, 2].astype(float)) # update sos_filter with any gcode parameter changes self._continuous_tare_filter_helper.update_from_command(gcmd) - self._mcu_trigger_analog.set_endstop_range(tare_counts, gcmd) + # update the load cell so it reflects the new tare value + self._load_cell.tare(tare_counts) + # update range and trigger + safety_min, safety_max = self._config_helper.get_safety_range(gcmd) + self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) + trigger_val = self._config_helper.get_trigger_force_grams(gcmd) + self._mcu_trigger_analog.set_trigger_value(trigger_val) + # update internal tare value + gpc = self._config_helper.get_grams_per_count() + sos_filter = self._mcu_trigger_analog.get_sos_filter() + Q17_14_FRAC_BITS = 14 + sos_filter.set_offset_scale(int(-tare_counts), gpc, + Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) + self._mcu_trigger_analog.reset_filter() def _home_start(self, print_time): # start trsync @@ -624,10 +642,10 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = MCU_trigger_analog(config, self._load_cell, + self._mcu_trigger_analog = MCU_trigger_analog(config, sensor, continuous_tare_filter_helper.get_sos_filter(), config_helper, trigger_dispatch) - load_cell_probing_move = LoadCellProbingMove(config, + load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) self._tapping_move = TappingMove(config, load_cell_probing_move, diff --git a/klippy/extras/sos_filter.py b/klippy/extras/sos_filter.py index 6456eaa5c..01a394377 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/sos_filter.py @@ -108,6 +108,9 @@ class MCU_SosFilter: def get_oid(self): return self._oid + def convert_value(self, val): + return to_fixed_32(val, self._value_frac_bits) + # convert the SciPi SOS filters to fixed point format def _convert_filter(self): if self._design is None: From 165fe1730d9d9ec902d8345b415fd50d3956f8ef Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 19:16:31 -0500 Subject: [PATCH 073/108] load_cell_probe: Move phoming.probing_move() interface to MCU_trigger_analog Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 127 ++++++++++++++----------------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 4daeec2a2..ea4aaf767 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -242,7 +242,6 @@ class LoadCellProbeConfigHelper: self._printer = config.get_printer() self._load_cell = load_cell_inst self._sensor = load_cell_inst.get_sensor() - self._rest_time = 1. / float(self._sensor.get_samples_per_second()) # Collect 4 x 60hz power cycles of data to average across power noise self._tare_time_param = floatParamHelper(config, 'tare_time', default=4. / 60., minval=0.01, maxval=1.0) @@ -263,9 +262,6 @@ class LoadCellProbeConfigHelper: def get_safety_limit_grams(self, gcmd=None): return self._force_safety_limit_param.get(gcmd) - def get_rest_time(self): - return self._rest_time - def get_safety_range(self, gcmd=None): counts_per_gram = self._load_cell.get_counts_per_gram() # calculate the safety band @@ -294,16 +290,21 @@ class LoadCellProbeConfigHelper: # MCU_trigger_analog is the interface to `trigger_analog` on the MCU # This also manages the SosFilter so all commands use one command queue class MCU_trigger_analog: - WATCHDOG_MAX = 3 + MONITOR_MAX = 3 ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 + ERROR_MAP = { + mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " + "homing", + ERROR_SAFETY_RANGE: "sensor exceeds safety limit", + ERROR_OVERFLOW: "fixed point math overflow", + ERROR_WATCHDOG: "timed out waiting for sensor data" + } - def __init__(self, config, sensor_inst, sos_filter_inst, config_helper, - trigger_dispatch): - self._printer = config.get_printer() + def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): + self._printer = sensor_inst.get_mcu().get_printer() self._sos_filter = sos_filter_inst - self._config_helper = config_helper self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() # configure MCU objects @@ -313,6 +314,7 @@ class MCU_trigger_analog: self._raw_min = self._raw_max = 0 self._last_range_args = None self._trigger_value = 0. + self._last_trigger_time = 0. self._config_commands() self._home_cmd = None self._query_cmd = None @@ -355,6 +357,9 @@ class MCU_trigger_analog: def get_dispatch(self): return self._dispatch + def get_last_trigger_time(self): + return self._last_trigger_time + def set_trigger_value(self, trigger_value): self._trigger_value = trigger_value @@ -362,7 +367,7 @@ class MCU_trigger_analog: self._raw_min = raw_min self._raw_max = raw_max - def reset_filter(self): + def _reset_filter(self): # Update parameters in mcu (if they have changed) tval32 = self._sos_filter.convert_value(self._trigger_value) args = [self._oid, self._raw_min, self._raw_max, tval32] @@ -372,15 +377,7 @@ class MCU_trigger_analog: # Update sos filter in mcu self._sos_filter.reset_filter() - def home_start(self, print_time): - clock = self._mcu.print_time_to_clock(print_time) - rest_time = self._config_helper.get_rest_time() - rest_ticks = self._mcu.seconds_to_clock(rest_time) - self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - rest_ticks, self.WATCHDOG_MAX], reqclock=clock) - - def clear_home(self): + def _clear_home(self): params = self._query_cmd.send([self._oid]) # The time of the first sample that triggered is in "trigger_ticks" trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) @@ -388,20 +385,43 @@ class MCU_trigger_analog: self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) return self._mcu.clock_to_print_time(trigger_ticks) + def get_steppers(self): + return self._dispatch.get_steppers() + + def home_start(self, print_time, sample_time, sample_count, rest_time, + triggered=True): + self._last_trigger_time = 0. + self._reset_filter() + trigger_completion = self._dispatch.start(print_time) + clock = self._mcu.print_time_to_clock(print_time) + sensor_update = 1. / self._sensor.get_samples_per_second() + sm_ticks = self._mcu.seconds_to_clock(sensor_update) + self._home_cmd.send([self._oid, self._dispatch.get_oid(), + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, + sm_ticks, self.MONITOR_MAX], reqclock=clock) + return trigger_completion + + def home_wait(self, home_end_time): + self._dispatch.wait_end(home_end_time) + # trigger has happened, now to find out why... + res = self._dispatch.stop() + # clear the homing state so it stops processing samples + trigger_time = self._clear_home() + if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + defmsg = "unknown reason code %i" % (res,) + error_msg = self.ERROR_MAP.get(res, defmsg) + raise self._printer.command_error("Trigger analog error: %s" + % (error_msg,)) + if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: + return 0. + if self._mcu.is_fileoutput(): + trigger_time = home_end_time + self._last_trigger_time = trigger_time + return trigger_time + # Execute probing moves using the MCU_trigger_analog class LoadCellProbingMove: - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - MCU_trigger_analog.ERROR_SAFETY_RANGE: "Load Cell Probe Error: load " - "exceeds safety limit", - MCU_trigger_analog.ERROR_OVERFLOW: "Load Cell Probe Error: fixed point " - "math overflow", - MCU_trigger_analog.ERROR_WATCHDOG: "Load Cell Probe Error: timed out " - "waiting for sensor data" - } - def __init__(self, config, load_cell_inst, mcu_trigger_analog, param_helper, continuous_tare_filter_helper, config_helper): self._printer = config.get_printer() @@ -416,7 +436,6 @@ class LoadCellProbingMove: probe.LookupZSteppers(config, self._dispatch.add_stepper) # internal state tracking self._tare_counts = 0 - self._last_trigger_time = 0 def _start_collector(self): toolhead = self._printer.lookup_object('toolhead') @@ -451,37 +470,6 @@ class LoadCellProbingMove: Q17_14_FRAC_BITS = 14 sos_filter.set_offset_scale(int(-tare_counts), gpc, Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) - self._mcu_trigger_analog.reset_filter() - - def _home_start(self, print_time): - # start trsync - trigger_completion = self._dispatch.start(print_time) - self._mcu_trigger_analog.home_start(print_time) - return trigger_completion - - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - return self._home_start(print_time) - - def home_wait(self, home_end_time): - self._dispatch.wait_end(home_end_time) - # trigger has happened, now to find out why... - res = self._dispatch.stop() - # clear the homing state so it stops processing samples - self._last_trigger_time = self._mcu_trigger_analog.clear_home() - if self._mcu.is_fileoutput(): - self._last_trigger_time = home_end_time - if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - error = "Load Cell Probe Error: unknown reason code %i" % (res,) - if res in self.ERROR_MAP: - error = self.ERROR_MAP[res] - raise self._printer.command_error(error) - if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. - return self._last_trigger_time - - def get_steppers(self): - return self._dispatch.get_steppers() # Probe towards z_min until the trigger_analog on the MCU triggers def probing_move(self, gcmd): @@ -499,20 +487,22 @@ class LoadCellProbingMove: # start collector after tare samples are consumed collector = self._start_collector() # do homing move - return phoming.probing_move(self, pos, speed), collector + epos = phoming.probing_move(self._mcu_trigger_analog, pos, speed) + return epos, collector # Wait for the MCU to trigger with no movement def probing_test(self, gcmd, timeout): self._pause_and_tare(gcmd) toolhead = self._printer.lookup_object('toolhead') print_time = toolhead.get_last_move_time() - self._home_start(print_time) - return self.home_wait(print_time + timeout) + self._mcu_trigger_analog.home_start(print_time, 0., 0, 0.) + return self._mcu_trigger_analog.home_wait(print_time + timeout) def get_status(self, eventtime): + trig_time = self._mcu_trigger_analog.get_last_trigger_time() return { 'tare_counts': self._tare_counts, - 'last_trigger_time': self._last_trigger_time, + 'last_trigger_time': trig_time, } @@ -642,9 +632,8 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = MCU_trigger_analog(config, sensor, - continuous_tare_filter_helper.get_sos_filter(), config_helper, - trigger_dispatch) + self._mcu_trigger_analog = MCU_trigger_analog(sensor, + continuous_tare_filter_helper.get_sos_filter(), trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) From 402303aa2287e9f07fe363f500bdf064476358e3 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 18:05:08 -0500 Subject: [PATCH 074/108] trigger_analog: New trigger_analog.py file Rename sos_filter.py to trigger_analog.py and copy MCU_trigger_analog class from load_cell_probe.py to this new file. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 143 +---------------- .../{sos_filter.py => trigger_analog.py} | 145 +++++++++++++++++- 2 files changed, 149 insertions(+), 139 deletions(-) rename klippy/extras/{sos_filter.py => trigger_analog.py} (58%) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index ea4aaf767..4d53a320a 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math import mcu -from . import probe, sos_filter, load_cell, hx71x, ads1220 +from . import probe, trigger_analog, load_cell, hx71x, ads1220 np = None # delay NumPy import until configuration time @@ -161,7 +161,7 @@ class ContinuousTareFilter: # create a filter design from the parameters def design_filter(self, error_func): - return sos_filter.DigitalFilter(self.sps, error_func, self.drift, + return trigger_analog.DigitalFilter(self.sps, error_func, self.drift, self.drift_delay, self.buzz, self.buzz_delay, self.notches, self.notch_quality) @@ -209,8 +209,8 @@ class ContinuousTareFilterHelper: buzz_delay, notches, notch_quality) def _create_filter(self, design, cmd_queue): - sf = sos_filter.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_29_FRAC_BITS) + sf = trigger_analog.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, + Q2_29_FRAC_BITS) sf.set_filter_design(design) return sf @@ -287,139 +287,6 @@ class LoadCellProbeConfigHelper: return 1. / counts_per_gram -# MCU_trigger_analog is the interface to `trigger_analog` on the MCU -# This also manages the SosFilter so all commands use one command queue -class MCU_trigger_analog: - MONITOR_MAX = 3 - ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 - ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - ERROR_SAFETY_RANGE: "sensor exceeds safety limit", - ERROR_OVERFLOW: "fixed point math overflow", - ERROR_WATCHDOG: "timed out waiting for sensor data" - } - - def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): - self._printer = sensor_inst.get_mcu().get_printer() - self._sos_filter = sos_filter_inst - self._sensor = sensor_inst - self._mcu = self._sensor.get_mcu() - # configure MCU objects - self._dispatch = trigger_dispatch - self._cmd_queue = self._dispatch.get_command_queue() - self._oid = self._mcu.create_oid() - self._raw_min = self._raw_max = 0 - self._last_range_args = None - self._trigger_value = 0. - self._last_trigger_time = 0. - self._config_commands() - self._home_cmd = None - self._query_cmd = None - self._set_range_cmd = None - self._mcu.register_config_callback(self._build_config) - self._printer.register_event_handler("klippy:connect", self._on_connect) - - def _config_commands(self): - self._mcu.add_config_cmd( - "config_trigger_analog oid=%d sos_filter_oid=%d" % ( - self._oid, self._sos_filter.get_oid())) - - def _build_config(self): - # Lookup commands - self._query_cmd = self._mcu.lookup_query_command( - "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) - self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) - self._home_cmd = self._mcu.lookup_command( - "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u", - cq=self._cmd_queue) - - # the sensor data stream is connected on the MCU at the ready event - def _on_connect(self): - self._sensor.attach_trigger_analog(self._oid) - - def get_oid(self): - return self._oid - - def get_mcu(self): - return self._mcu - - def get_sos_filter(self): - return self._sos_filter - - def get_dispatch(self): - return self._dispatch - - def get_last_trigger_time(self): - return self._last_trigger_time - - def set_trigger_value(self, trigger_value): - self._trigger_value = trigger_value - - def set_raw_range(self, raw_min, raw_max): - self._raw_min = raw_min - self._raw_max = raw_max - - def _reset_filter(self): - # Update parameters in mcu (if they have changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._raw_min, self._raw_max, tval32] - if args != self._last_range_args: - self._set_range_cmd.send(args) - self._last_range_args = args - # Update sos filter in mcu - self._sos_filter.reset_filter() - - def _clear_home(self): - params = self._query_cmd.send([self._oid]) - # The time of the first sample that triggered is in "trigger_ticks" - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) - # clear trsync from load_cell_endstop - self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) - return self._mcu.clock_to_print_time(trigger_ticks) - - def get_steppers(self): - return self._dispatch.get_steppers() - - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - self._last_trigger_time = 0. - self._reset_filter() - trigger_completion = self._dispatch.start(print_time) - clock = self._mcu.print_time_to_clock(print_time) - sensor_update = 1. / self._sensor.get_samples_per_second() - sm_ticks = self._mcu.seconds_to_clock(sensor_update) - self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - sm_ticks, self.MONITOR_MAX], reqclock=clock) - return trigger_completion - - def home_wait(self, home_end_time): - self._dispatch.wait_end(home_end_time) - # trigger has happened, now to find out why... - res = self._dispatch.stop() - # clear the homing state so it stops processing samples - trigger_time = self._clear_home() - if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - defmsg = "unknown reason code %i" % (res,) - error_msg = self.ERROR_MAP.get(res, defmsg) - raise self._printer.command_error("Trigger analog error: %s" - % (error_msg,)) - if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. - if self._mcu.is_fileoutput(): - trigger_time = home_end_time - self._last_trigger_time = trigger_time - return trigger_time - - # Execute probing moves using the MCU_trigger_analog class LoadCellProbingMove: def __init__(self, config, load_cell_inst, mcu_trigger_analog, param_helper, @@ -632,7 +499,7 @@ class LoadCellPrinterProbe: self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = MCU_trigger_analog(sensor, + self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor, continuous_tare_filter_helper.get_sos_filter(), trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, diff --git a/klippy/extras/sos_filter.py b/klippy/extras/trigger_analog.py similarity index 58% rename from klippy/extras/sos_filter.py rename to klippy/extras/trigger_analog.py index 01a394377..482fa6272 100644 --- a/klippy/extras/sos_filter.py +++ b/klippy/extras/trigger_analog.py @@ -1,8 +1,15 @@ -# Second Order Sections Filter +# Wrapper around mcu trigger_analog objects # # Copyright (C) 2025 Gareth Farrington +# Copyright (C) 2026 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. +import mcu + + +###################################################################### +# SOS filters (Second Order Sectional) +###################################################################### MAX_INT32 = (2 ** 31) MIN_INT32 = -(2 ** 31) - 1 @@ -196,3 +203,139 @@ class MCU_SosFilter: # Activate filter self._set_active_cmd.send([self._oid, num_sections, self._coeff_frac_bits]) + + +###################################################################### +# Trigger Analog +###################################################################### + +# MCU_trigger_analog is the interface to `trigger_analog` on the MCU +class MCU_trigger_analog: + MONITOR_MAX = 3 + ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 + ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 + ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 + ERROR_MAP = { + mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " + "homing", + ERROR_SAFETY_RANGE: "sensor exceeds safety limit", + ERROR_OVERFLOW: "fixed point math overflow", + ERROR_WATCHDOG: "timed out waiting for sensor data" + } + + def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): + self._printer = sensor_inst.get_mcu().get_printer() + self._sos_filter = sos_filter_inst + self._sensor = sensor_inst + self._mcu = self._sensor.get_mcu() + # configure MCU objects + self._dispatch = trigger_dispatch + self._cmd_queue = self._dispatch.get_command_queue() + self._oid = self._mcu.create_oid() + self._raw_min = self._raw_max = 0 + self._last_range_args = None + self._trigger_value = 0. + self._last_trigger_time = 0. + self._config_commands() + self._home_cmd = None + self._query_cmd = None + self._set_range_cmd = None + self._mcu.register_config_callback(self._build_config) + self._printer.register_event_handler("klippy:connect", self._on_connect) + + def _config_commands(self): + self._mcu.add_config_cmd( + "config_trigger_analog oid=%d sos_filter_oid=%d" % ( + self._oid, self._sos_filter.get_oid())) + + def _build_config(self): + # Lookup commands + self._query_cmd = self._mcu.lookup_query_command( + "trigger_analog_query_state oid=%c", + "trigger_analog_state oid=%c is_homing_trigger=%c " + "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) + self._set_range_cmd = self._mcu.lookup_command( + "trigger_analog_set_range oid=%c safety_counts_min=%i" + " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) + self._home_cmd = self._mcu.lookup_command( + "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" + " error_reason=%c clock=%u rest_ticks=%u timeout=%u", + cq=self._cmd_queue) + + # the sensor data stream is connected on the MCU at the ready event + def _on_connect(self): + self._sensor.attach_trigger_analog(self._oid) + + def get_oid(self): + return self._oid + + def get_mcu(self): + return self._mcu + + def get_sos_filter(self): + return self._sos_filter + + def get_dispatch(self): + return self._dispatch + + def get_last_trigger_time(self): + return self._last_trigger_time + + def set_trigger_value(self, trigger_value): + self._trigger_value = trigger_value + + def set_raw_range(self, raw_min, raw_max): + self._raw_min = raw_min + self._raw_max = raw_max + + def _reset_filter(self): + # Update parameters in mcu (if they have changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._raw_min, self._raw_max, tval32] + if args != self._last_range_args: + self._set_range_cmd.send(args) + self._last_range_args = args + # Update sos filter in mcu + self._sos_filter.reset_filter() + + def _clear_home(self): + params = self._query_cmd.send([self._oid]) + # The time of the first sample that triggered is in "trigger_ticks" + trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) + # clear trsync from load_cell_endstop + self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) + return self._mcu.clock_to_print_time(trigger_ticks) + + def get_steppers(self): + return self._dispatch.get_steppers() + + def home_start(self, print_time, sample_time, sample_count, rest_time, + triggered=True): + self._last_trigger_time = 0. + self._reset_filter() + trigger_completion = self._dispatch.start(print_time) + clock = self._mcu.print_time_to_clock(print_time) + sensor_update = 1. / self._sensor.get_samples_per_second() + sm_ticks = self._mcu.seconds_to_clock(sensor_update) + self._home_cmd.send([self._oid, self._dispatch.get_oid(), + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, + sm_ticks, self.MONITOR_MAX], reqclock=clock) + return trigger_completion + + def home_wait(self, home_end_time): + self._dispatch.wait_end(home_end_time) + # trigger has happened, now to find out why... + res = self._dispatch.stop() + # clear the homing state so it stops processing samples + trigger_time = self._clear_home() + if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + defmsg = "unknown reason code %i" % (res,) + error_msg = self.ERROR_MAP.get(res, defmsg) + raise self._printer.command_error("Trigger analog error: %s" + % (error_msg,)) + if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: + return 0. + if self._mcu.is_fileoutput(): + trigger_time = home_end_time + self._last_trigger_time = trigger_time + return trigger_time From f9b1b9c1b5775d8a7a2468c837a65ab9ac994f33 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 19:41:43 -0500 Subject: [PATCH 075/108] trigger_analog: Create trigger_dispatch within MCU_trigger_analog Don't require callers of MCU_trigger_analog to create the mcu.TriggerDispatch() instance - instead, create it within the MCU_trigger_analog() class. Also, make it easier to use MCU_trigger_analog without an MCU_SosFilter - the MCU_trigger_analog can automatically create an empty filter if needed. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 32 +++++++++++++------------------ klippy/extras/trigger_analog.py | 33 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 4d53a320a..c15a57fd7 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -168,8 +168,9 @@ class ContinuousTareFilter: # Combine ContinuousTareFilter and SosFilter into an easy-to-use class class ContinuousTareFilterHelper: - def __init__(self, config, sensor, cmd_queue): + def __init__(self, config, sensor, sos_filter): self._sensor = sensor + self._sos_filter = sos_filter self._sps = self._sensor.get_samples_per_second() max_filter_frequency = math.floor(self._sps / 2.) # setup filter parameters @@ -194,8 +195,8 @@ class ContinuousTareFilterHelper: self._config_design = self._build_filter() # filter design currently inside the MCU self._active_design = self._config_design - self._sos_filter = self._create_filter( - self._active_design.design_filter(config.error), cmd_queue) + design = self._active_design.design_filter(config.error) + self._sos_filter.set_filter_design(design) def _build_filter(self, gcmd=None): drift = self._drift_param.get(gcmd) @@ -208,12 +209,6 @@ class ContinuousTareFilterHelper: return ContinuousTareFilter(self._sps, drift, drift_delay, buzz, buzz_delay, notches, notch_quality) - def _create_filter(self, design, cmd_queue): - sf = trigger_analog.MCU_SosFilter(self._sensor.get_mcu(), cmd_queue, 4, - Q2_29_FRAC_BITS) - sf.set_filter_design(design) - return sf - def update_from_command(self, gcmd, cq=None): gcmd_filter = self._build_filter(gcmd) # if filters are identical, no change required @@ -223,9 +218,6 @@ class ContinuousTareFilterHelper: design = self._active_design.design_filter(gcmd.error) self._sos_filter.set_filter_design(design) - def get_sos_filter(self): - return self._sos_filter - # check results from the collector for errors and raise an exception is found def check_sensor_errors(results, printer): @@ -299,8 +291,8 @@ class LoadCellProbingMove: self._config_helper = config_helper self._mcu = mcu_trigger_analog.get_mcu() self._z_min_position = probe.lookup_minimum_z(config) - self._dispatch = mcu_trigger_analog.get_dispatch() - probe.LookupZSteppers(config, self._dispatch.add_stepper) + dispatch = mcu_trigger_analog.get_dispatch() + probe.LookupZSteppers(config, dispatch.add_stepper) # internal state tracking self._tare_counts = 0 @@ -492,15 +484,17 @@ class LoadCellPrinterProbe: # Read all user configuration and build modules config_helper = LoadCellProbeConfigHelper(config, self._load_cell) self._mcu = self._load_cell.get_sensor().get_mcu() - trigger_dispatch = mcu.TriggerDispatch(self._mcu) - continuous_tare_filter_helper = ContinuousTareFilterHelper(config, - sensor, trigger_dispatch.get_command_queue()) + self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor) + cmd_queue = self._mcu_trigger_analog.get_dispatch().get_command_queue() + sos_filter = trigger_analog.MCU_SosFilter(self._mcu, cmd_queue, 4, + Q2_29_FRAC_BITS) + self._mcu_trigger_analog.setup_sos_filter(sos_filter) + continuous_tare_filter_helper = ContinuousTareFilterHelper( + config, sensor, sos_filter) # Probe Interface self._param_helper = probe.ProbeParameterHelper(config) self._cmd_helper = probe.ProbeCommandHelper(config, self) self._probe_offsets = probe.ProbeOffsetsHelper(config) - self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor, - continuous_tare_filter_helper.get_sos_filter(), trigger_dispatch) load_cell_probing_move = LoadCellProbingMove(config, self._load_cell, self._mcu_trigger_analog, self._param_helper, continuous_tare_filter_helper, config_helper) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 482fa6272..78c4e7c70 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -201,8 +201,9 @@ class MCU_SosFilter: self._set_offset_scale_cmd.send(args) self._last_sent_offset_scale = args # Activate filter - self._set_active_cmd.send([self._oid, num_sections, - self._coeff_frac_bits]) + if self._max_sections: + self._set_active_cmd.send([self._oid, num_sections, + self._coeff_frac_bits]) ###################################################################### @@ -223,44 +224,44 @@ class MCU_trigger_analog: ERROR_WATCHDOG: "timed out waiting for sensor data" } - def __init__(self, sensor_inst, sos_filter_inst, trigger_dispatch): + def __init__(self, sensor_inst): self._printer = sensor_inst.get_mcu().get_printer() - self._sos_filter = sos_filter_inst self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() + self._sos_filter = None # configure MCU objects - self._dispatch = trigger_dispatch - self._cmd_queue = self._dispatch.get_command_queue() + self._dispatch = mcu.TriggerDispatch(self._mcu) self._oid = self._mcu.create_oid() self._raw_min = self._raw_max = 0 self._last_range_args = None self._trigger_value = 0. self._last_trigger_time = 0. - self._config_commands() - self._home_cmd = None - self._query_cmd = None - self._set_range_cmd = None + self._home_cmd = self._query_cmd = self._set_range_cmd = None self._mcu.register_config_callback(self._build_config) self._printer.register_event_handler("klippy:connect", self._on_connect) - def _config_commands(self): + def setup_sos_filter(self, sos_filter): + self._sos_filter = sos_filter + + def _build_config(self): + cmd_queue = self._dispatch.get_command_queue() + if self._sos_filter is None: + self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0, 0)) self._mcu.add_config_cmd( "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) - - def _build_config(self): # Lookup commands self._query_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=self._cmd_queue) + "trigger_ticks=%u", oid=self._oid, cq=cmd_queue) self._set_range_cmd = self._mcu.lookup_command( "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=self._cmd_queue) + " safety_counts_max=%i trigger_value=%i", cq=cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" " error_reason=%c clock=%u rest_ticks=%u timeout=%u", - cq=self._cmd_queue) + cq=cmd_queue) # the sensor data stream is connected on the MCU at the ready event def _on_connect(self): From 5bd791d96e705c7506cb48ca1be157c01911bfc8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 20:02:21 -0500 Subject: [PATCH 076/108] hx71x: Make sure to use the same cmd_queue for all commands Signed-off-by: Kevin O'Connor --- klippy/extras/hx71x.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index 60fe6047c..f0d904a89 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -63,13 +63,14 @@ class HX71xBase: mcu.register_config_callback(self._build_config) def _build_config(self): + cmd_queue = self.mcu.alloc_command_queue() self.query_hx71x_cmd = self.mcu.lookup_command( - "query_hx71x oid=%c rest_ticks=%u") + "query_hx71x oid=%c rest_ticks=%u", cq=cmd_queue) self.attach_probe_cmd = self.mcu.lookup_command( - "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c") + "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c", + cq=cmd_queue) self.ffreader.setup_query_command("query_hx71x_status oid=%c", - oid=self.oid, - cq=self.mcu.alloc_command_queue()) + oid=self.oid, cq=cmd_queue) def get_mcu(self): From 87c8b505a7f77d03a8b6b15e733628ebe504fe08 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 20:10:47 -0500 Subject: [PATCH 077/108] trigger_analog: Attach trigger_analog to sensor during initialization Avoid setting up a "connect" callback - just register the association using mcu.add_config_cmd() . Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 11 +++++------ klippy/extras/hx71x.py | 12 +++++------- klippy/extras/trigger_analog.py | 6 +----- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index fda584ac3..809c44435 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -96,7 +96,6 @@ class ADS1220: self.printer, self._process_batch, self._start_measurements, self._finish_measurements, UPDATE_INTERVAL) # Command Configuration - self.attach_probe_cmd = None mcu.add_config_cmd( "config_ads1220 oid=%d spi_oid=%d data_ready_pin=%s" % (self.oid, self.spi.get_oid(), self.data_ready_pin)) @@ -105,12 +104,15 @@ class ADS1220: mcu.register_config_callback(self._build_config) self.query_ads1220_cmd = None + def setup_trigger_analog(self, trigger_analog_oid): + self.mcu.add_config_cmd( + "ads1220_attach_trigger_analog oid=%d trigger_analog_oid=%d" + % (self.oid, trigger_analog_oid), is_init=True) + def _build_config(self): cmdqueue = self.spi.get_command_queue() self.query_ads1220_cmd = self.mcu.lookup_command( "query_ads1220 oid=%c rest_ticks=%u", cq=cmdqueue) - self.attach_probe_cmd = self.mcu.lookup_command( - "ads1220_attach_trigger_analog oid=%c trigger_analog_oid=%c") self.ffreader.setup_query_command("query_ads1220_status oid=%c", oid=self.oid, cq=cmdqueue) @@ -129,9 +131,6 @@ class ADS1220: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_trigger_analog(self, trigger_analog_oid): - self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) - # Measurement decoding def _convert_samples(self, samples): adc_factor = 1. / (1 << 23) diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index f0d904a89..e9d7bf605 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -53,7 +53,6 @@ class HX71xBase: self._finish_measurements, UPDATE_INTERVAL) # Command Configuration self.query_hx71x_cmd = None - self.attach_probe_cmd = None mcu.add_config_cmd( "config_hx71x oid=%d gain_channel=%d dout_pin=%s sclk_pin=%s" % (self.oid, self.gain_channel, self.dout_pin, self.sclk_pin)) @@ -62,13 +61,15 @@ class HX71xBase: mcu.register_config_callback(self._build_config) + def setup_trigger_analog(self, trigger_analog_oid): + self.mcu.add_config_cmd( + "hx71x_attach_trigger_analog oid=%d trigger_analog_oid=%d" + % (self.oid, trigger_analog_oid), is_init=True) + def _build_config(self): cmd_queue = self.mcu.alloc_command_queue() self.query_hx71x_cmd = self.mcu.lookup_command( "query_hx71x oid=%c rest_ticks=%u", cq=cmd_queue) - self.attach_probe_cmd = self.mcu.lookup_command( - "hx71x_attach_trigger_analog oid=%c trigger_analog_oid=%c", - cq=cmd_queue) self.ffreader.setup_query_command("query_hx71x_status oid=%c", oid=self.oid, cq=cmd_queue) @@ -88,9 +89,6 @@ class HX71xBase: def add_client(self, callback): self.batch_bulk.add_client(callback) - def attach_trigger_analog(self, trigger_analog_oid): - self.attach_probe_cmd.send([self.oid, trigger_analog_oid]) - # Measurement decoding def _convert_samples(self, samples): adc_factor = 1. / (1 << 23) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 78c4e7c70..4e149cdf0 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -238,12 +238,12 @@ class MCU_trigger_analog: self._last_trigger_time = 0. self._home_cmd = self._query_cmd = self._set_range_cmd = None self._mcu.register_config_callback(self._build_config) - self._printer.register_event_handler("klippy:connect", self._on_connect) def setup_sos_filter(self, sos_filter): self._sos_filter = sos_filter def _build_config(self): + self._sensor.setup_trigger_analog(self._oid) cmd_queue = self._dispatch.get_command_queue() if self._sos_filter is None: self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0, 0)) @@ -263,10 +263,6 @@ class MCU_trigger_analog: " error_reason=%c clock=%u rest_ticks=%u timeout=%u", cq=cmd_queue) - # the sensor data stream is connected on the MCU at the ready event - def _on_connect(self): - self._sensor.attach_trigger_analog(self._oid) - def get_oid(self): return self._oid From 73a6184407996fe3553daf70182f5e116f0b8a54 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 21:17:59 -0500 Subject: [PATCH 078/108] trigger_analog: Check if trigger_analog is allocated in trigger_analog_update() Check if the trigger_analog struct has been allocated in trigger_analog_update() itself. This makes the code easier to use in the sensor code. Signed-off-by: Kevin O'Connor --- src/sensor_ads1220.c | 4 +--- src/sensor_hx71x.c | 2 +- src/trigger_analog.c | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c index 93d52b6ae..fdf770cf6 100644 --- a/src/sensor_ads1220.c +++ b/src/sensor_ads1220.c @@ -97,9 +97,7 @@ ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid) counts |= 0xFF000000; // endstop is optional, report if enabled and no errors - if (ads1220->ta) { - trigger_analog_update(ads1220->ta, counts); - } + trigger_analog_update(ads1220->ta, counts); add_sample(ads1220, oid, counts); } diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c index 2c09e97af..7d869d9d5 100644 --- a/src/sensor_hx71x.c +++ b/src/sensor_hx71x.c @@ -178,7 +178,7 @@ hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid) } // probe is optional, report if enabled - if (hx71x->last_error == 0 && hx71x->ta) { + if (hx71x->last_error == 0) { trigger_analog_update(hx71x->ta, counts); } diff --git a/src/trigger_analog.c b/src/trigger_analog.c index df64a4f40..fafc63fc6 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -76,6 +76,9 @@ trigger_error(struct trigger_analog *ta, uint8_t error_code) void trigger_analog_update(struct trigger_analog *ta, const int32_t sample) { + if (!ta) + return; + // only process samples when homing uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); if (!is_homing) { From 147022dee2e18b1b98ab478a5f00e390bcb58b30 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 22:16:22 -0500 Subject: [PATCH 079/108] trigger_analog: Update to support generic trigger types Rework the trigger_analog code to support different "trigger" conditions. This merges in features of ldc1612.c into trigger_analog.c, such as error code reporting in the MCU. This is in preparation for using trigger_analog with ldc1612. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 3 + klippy/extras/hx71x.py | 3 + klippy/extras/load_cell_probe.py | 2 +- klippy/extras/trigger_analog.py | 87 ++++++---- src/trigger_analog.c | 265 +++++++++++++++++-------------- src/trigger_analog.h | 1 + 6 files changed, 202 insertions(+), 159 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 809c44435..891783922 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -122,6 +122,9 @@ class ADS1220: def get_samples_per_second(self): return self.sps + def lookup_sensor_error(self, error_code): + return "Unknown ads1220 error" % (error_code,) + # returns a tuple of the minimum and maximum value of the sensor, used to # detect if a data value is saturated def get_range(self): diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index e9d7bf605..a1bc20529 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -80,6 +80,9 @@ class HX71xBase: def get_samples_per_second(self): return self.sps + def lookup_sensor_error(self, error_code): + return "Unknown hx71x error %d" % (error_code,) + # returns a tuple of the minimum and maximum value of the sensor, used to # detect if a data value is saturated def get_range(self): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index c15a57fd7..5f2a3c111 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -322,7 +322,7 @@ class LoadCellProbingMove: safety_min, safety_max = self._config_helper.get_safety_range(gcmd) self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - self._mcu_trigger_analog.set_trigger_value(trigger_val) + self._mcu_trigger_analog.set_trigger("abs_ge", trigger_val) # update internal tare value gpc = self._config_helper.get_grams_per_count() sos_filter = self._mcu_trigger_analog.get_sos_filter() diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 4e149cdf0..2a6952ccb 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -213,30 +213,28 @@ class MCU_SosFilter: # MCU_trigger_analog is the interface to `trigger_analog` on the MCU class MCU_trigger_analog: MONITOR_MAX = 3 - ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 - ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - ERROR_SAFETY_RANGE: "sensor exceeds safety limit", - ERROR_OVERFLOW: "fixed point math overflow", - ERROR_WATCHDOG: "timed out waiting for sensor data" - } - + REASON_TRIGGER_ANALOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, sensor_inst): self._printer = sensor_inst.get_mcu().get_printer() self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() self._sos_filter = None - # configure MCU objects self._dispatch = mcu.TriggerDispatch(self._mcu) - self._oid = self._mcu.create_oid() + self._last_trigger_time = 0. + # Raw range checking self._raw_min = self._raw_max = 0 self._last_range_args = None + # Trigger type + self._trigger_type = "unspecified" self._trigger_value = 0. - self._last_trigger_time = 0. - self._home_cmd = self._query_cmd = self._set_range_cmd = None + self._last_trigger_args = None + # Error codes from MCU + self._error_map = {} + self._sensor_specific_error = 0 + # Configure MCU objects + self._oid = self._mcu.create_oid() + self._home_cmd = self._query_state_cmd = None + self._set_raw_range_cmd = self._set_trigger_cmd = None self._mcu.register_config_callback(self._build_config) def setup_sos_filter(self, sos_filter): @@ -251,17 +249,24 @@ class MCU_trigger_analog: "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) # Lookup commands - self._query_cmd = self._mcu.lookup_query_command( + self._query_state_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=cmd_queue) - self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=cmd_queue) + "trigger_analog_state oid=%c homing=%c trigger_clock=%u", + oid=self._oid, cq=cmd_queue) + self._set_raw_range_cmd = self._mcu.lookup_command( + "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i", + cq=cmd_queue) + self._set_trigger_cmd = self._mcu.lookup_command( + "trigger_analog_set_trigger oid=%c trigger_analog_type=%c" + " trigger_value=%i", cq=cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u", + " error_reason=%c clock=%u monitor_ticks=%u monitor_max=%u", cq=cmd_queue) + # Load errors from mcu + errors = self._mcu.get_enumerations().get("trigger_analog_error:", {}) + self._error_map = {v: k for k, v in errors.items()} + self._sensor_specific_error = errors.get("SENSOR_SPECIFIC", 0) def get_oid(self): return self._oid @@ -278,7 +283,8 @@ class MCU_trigger_analog: def get_last_trigger_time(self): return self._last_trigger_time - def set_trigger_value(self, trigger_value): + def set_trigger(self, trigger_type, trigger_value): + self._trigger_type = trigger_type self._trigger_value = trigger_value def set_raw_range(self, raw_min, raw_max): @@ -286,21 +292,24 @@ class MCU_trigger_analog: self._raw_max = raw_max def _reset_filter(self): - # Update parameters in mcu (if they have changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._raw_min, self._raw_max, tval32] + # Update raw range parameters in mcu (if they have changed) + args = [self._oid, self._raw_min, self._raw_max] if args != self._last_range_args: - self._set_range_cmd.send(args) + self._set_raw_range_cmd.send(args) self._last_range_args = args + # Update trigger in mcu (if it has changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._trigger_type, tval32] + if args != self._last_trigger_args: + self._set_trigger_cmd.send(args) + self._last_trigger_args = args # Update sos filter in mcu self._sos_filter.reset_filter() def _clear_home(self): - params = self._query_cmd.send([self._oid]) - # The time of the first sample that triggered is in "trigger_ticks" - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) - # clear trsync from load_cell_endstop self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) + params = self._query_state_cmd.send([self._oid]) + trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_clock']) return self._mcu.clock_to_print_time(trigger_ticks) def get_steppers(self): @@ -315,8 +324,8 @@ class MCU_trigger_analog: sensor_update = 1. / self._sensor.get_samples_per_second() sm_ticks = self._mcu.seconds_to_clock(sensor_update) self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - sm_ticks, self.MONITOR_MAX], reqclock=clock) + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_TRIGGER_ANALOG, + clock, sm_ticks, self.MONITOR_MAX], reqclock=clock) return trigger_completion def home_wait(self, home_end_time): @@ -326,8 +335,16 @@ class MCU_trigger_analog: # clear the homing state so it stops processing samples trigger_time = self._clear_home() if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - defmsg = "unknown reason code %i" % (res,) - error_msg = self.ERROR_MAP.get(res, defmsg) + if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + raise self._printer.command_error( + "Communication timeout during homing") + error_code = res - self.REASON_TRIGGER_ANALOG + if error_code >= self._sensor_specific_error: + sensor_err = error_code - self._sensor_specific_error + error_msg = self._sensor.lookup_sensor_error(sensor_err) + else: + defmsg = "Unknown code %i" % (error_code,) + error_msg = self._error_map.get(error_code, defmsg) raise self._printer.command_error("Trigger analog error: %s" % (error_msg,)) if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: diff --git a/src/trigger_analog.c b/src/trigger_analog.c index fafc63fc6..f7ec477ec 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -1,11 +1,13 @@ // Support homing/probing "trigger" notification from analog sensors // // Copyright (C) 2025 Gareth Farrington +// Copyright (C) 2024-2026 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. #include // abs #include "basecmd.h" // oid_alloc +#include "board/io.h" // writeb #include "board/misc.h" // timer_read_time #include "command.h" // DECL_COMMAND #include "sched.h" // shutdown @@ -13,133 +15,149 @@ #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger -#define ERROR_SAFETY_RANGE 0 -#define ERROR_OVERFLOW 1 -#define ERROR_WATCHDOG 2 - -// Flags -enum {FLAG_IS_HOMING = 1 << 0 - , FLAG_IS_HOMING_TRIGGER = 1 << 1 - , FLAG_AWAIT_HOMING = 1 << 2 -}; - -// Endstop Structure +// Main trigger_analog storage struct trigger_analog { - struct timer time; - uint32_t trigger_ticks, last_sample_ticks, rest_ticks; - uint32_t homing_start_time; - struct trsync *ts; - int32_t safety_counts_min, safety_counts_max; - uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; - int32_t trigger_value; + // Raw value range check + int32_t raw_min, raw_max; + // Filtering struct sos_filter *sf; + // Trigger value checking + int32_t trigger_value; + uint8_t trigger_type; + // Trsync triggering + uint8_t flags, trigger_reason, error_reason; + struct trsync *ts; + uint32_t homing_clock, trigger_clock; + // Sensor activity monitoring + uint8_t monitor_max, monitor_count; + struct timer time; + uint32_t monitor_ticks; }; -static inline uint8_t -is_flag_set(const uint8_t mask, struct trigger_analog *ta) -{ - return !!(mask & ta->flags); -} +// Homing flags +enum { + TA_AWAIT_HOMING = 1<<1, TA_CAN_TRIGGER = 1<<2 +}; -static inline void -set_flag(uint8_t mask, struct trigger_analog *ta) -{ - ta->flags |= mask; -} +// Trigger types +enum { + TT_ABS_GE, TT_GT +}; +DECL_ENUMERATION("trigger_analog_type", "abs_ge", TT_ABS_GE); +DECL_ENUMERATION("trigger_analog_type", "gt", TT_GT); -static inline void -clear_flag(uint8_t mask, struct trigger_analog *ta) -{ - ta->flags &= ~mask; -} +// Sample errors sent via trsync error code +enum { + TE_RAW_RANGE, TE_OVERFLOW, TE_MONITOR, TE_SENSOR_SPECIFIC +}; +DECL_ENUMERATION("trigger_analog_error:", "RAW_RANGE", TE_RAW_RANGE); +DECL_ENUMERATION("trigger_analog_error:", "OVERFLOW", TE_OVERFLOW); +DECL_ENUMERATION("trigger_analog_error:", "MONITOR", TE_MONITOR); +DECL_ENUMERATION("trigger_analog_error:", "SENSOR_SPECIFIC" + , TE_SENSOR_SPECIFIC); -void -try_trigger(struct trigger_analog *ta, uint32_t ticks) +// Timer callback that monitors for sensor timeouts +static uint_fast8_t +monitor_event(struct timer *t) { - uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); - if (!is_homing_triggered) { - // the first triggering sample when homing sets the trigger time - ta->trigger_ticks = ticks; - // this flag latches until a reset, disabling further triggering - set_flag(FLAG_IS_HOMING_TRIGGER, ta); - trsync_do_trigger(ta->ts, ta->trigger_reason); + struct trigger_analog *ta = container_of(t, struct trigger_analog, time); + + if (!(ta->flags & TA_CAN_TRIGGER)) + return SF_DONE; + + if (ta->monitor_count > ta->monitor_max) { + trsync_do_trigger(ta->ts, ta->error_reason + TE_MONITOR); + return SF_DONE; } + + // A sample was recently delivered, continue monitoring + ta->monitor_count++; + ta->time.waketime += ta->monitor_ticks; + return SF_RESCHEDULE; } -void -trigger_error(struct trigger_analog *ta, uint8_t error_code) +// Note recent activity +static void +monitor_note_activity(struct trigger_analog *ta) { + writeb(&ta->monitor_count, 0); +} + +// Check if a value should signal a "trigger" event +static int +check_trigger(struct trigger_analog *ta, int32_t value) +{ + switch (ta->trigger_type) { + case TT_ABS_GE: + return abs(value) >= ta->trigger_value; + case TT_GT: + return value > ta->trigger_value; + } + return 0; +} + +// Stop homing due to an error +static void +cancel_homing(struct trigger_analog *ta, uint8_t error_code) +{ + if (!(ta->flags & TA_CAN_TRIGGER)) + return; + ta->flags = 0; trsync_do_trigger(ta->ts, ta->error_reason + error_code); } +// Handle an error reported by the sensor +void +trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code) +{ + if (!ta) + return; + cancel_homing(ta, sensor_code + TE_SENSOR_SPECIFIC); +} + // Used by Sensors to report new raw ADC sample void -trigger_analog_update(struct trigger_analog *ta, const int32_t sample) +trigger_analog_update(struct trigger_analog *ta, int32_t sample) { + // Check homing is active if (!ta) return; + uint8_t flags = ta->flags; + if (!(flags & TA_CAN_TRIGGER)) + return; - // only process samples when homing - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); - if (!is_homing) { + // Check if homing has started + uint32_t time = timer_read_time(); + if ((flags & TA_AWAIT_HOMING) && timer_is_before(time, ta->homing_clock)) + return; + flags &= ~TA_AWAIT_HOMING; + + // Reset the sensor timeout checking + monitor_note_activity(ta); + + // Check that raw value is in range + if (sample < ta->raw_min || sample > ta->raw_max) { + cancel_homing(ta, TE_RAW_RANGE); return; } - // save new sample - uint32_t ticks = timer_read_time(); - ta->last_sample_ticks = ticks; - ta->watchdog_count = 0; - - // do not trigger before homing start time - uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, ta); - if (await_homing && timer_is_before(ticks, ta->homing_start_time)) { - return; - } - clear_flag(FLAG_AWAIT_HOMING, ta); - - // check for safety limit violations - const uint8_t is_safety_trigger = sample <= ta->safety_counts_min - || sample >= ta->safety_counts_max; - // too much force, this is an error while homing - if (is_safety_trigger) { - trigger_error(ta, ERROR_SAFETY_RANGE); - return; - } - - // perform filtering + // Perform filtering int32_t filtered_value = sample; int ret = sos_filter_apply(ta->sf, &filtered_value); if (ret) { - trigger_error(ta, ERROR_OVERFLOW); + cancel_homing(ta, TE_OVERFLOW); return; } - // update trigger state - if (abs(filtered_value) >= ta->trigger_value) { - try_trigger(ta, ta->last_sample_ticks); - } -} - -// Timer callback that monitors for timeouts -static uint_fast8_t -watchdog_event(struct timer *t) -{ - struct trigger_analog *ta = container_of(t, struct trigger_analog, time); - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); - uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); - // the watchdog stops when not homing or when trsync becomes triggered - if (!is_homing || is_homing_trigger) { - return SF_DONE; + // Check if this is a "trigger" + ret = check_trigger(ta, filtered_value); + if (ret) { + trsync_do_trigger(ta->ts, ta->trigger_reason); + ta->trigger_clock = time; + flags = 0; } - if (ta->watchdog_count > ta->watchdog_max) { - trigger_error(ta, ERROR_WATCHDOG); - } - ta->watchdog_count += 1; - - // A sample was recently delivered, continue monitoring - ta->time.waketime += ta->rest_ticks; - return SF_RESCHEDULE; + ta->flags = flags; } // Create a trigger_analog @@ -160,17 +178,27 @@ trigger_analog_oid_lookup(uint8_t oid) return oid_lookup(oid, command_config_trigger_analog); } -// Set the triggering range and tare value +// Set valid raw range void -command_trigger_analog_set_range(uint32_t *args) +command_trigger_analog_set_raw_range(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - ta->safety_counts_min = args[1]; - ta->safety_counts_max = args[2]; - ta->trigger_value = args[3]; + ta->raw_min = args[1]; + ta->raw_max = args[2]; } -DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i trigger_value=%i"); +DECL_COMMAND(command_trigger_analog_set_raw_range, + "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i"); + +// Set the triggering type and value +void +command_trigger_analog_set_trigger(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + ta->trigger_type = args[1]; + ta->trigger_value = args[2]; +} +DECL_COMMAND(command_trigger_analog_set_trigger, "trigger_analog_set_trigger" + " oid=%c trigger_analog_type=%c trigger_value=%i"); // Home an axis void @@ -178,42 +206,33 @@ command_trigger_analog_home(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); sched_del_timer(&ta->time); - // clear the homing trigger flag - clear_flag(FLAG_IS_HOMING_TRIGGER, ta); - clear_flag(FLAG_IS_HOMING, ta); - ta->trigger_ticks = 0; - ta->ts = NULL; - // 0 samples indicates homing is finished - if (args[3] == 0) { - // Disable end stop checking + ta->monitor_ticks = args[5]; + if (!ta->monitor_ticks) { + ta->flags = 0; + ta->ts = NULL; return; } ta->ts = trsync_oid_lookup(args[1]); ta->trigger_reason = args[2]; ta->error_reason = args[3]; - ta->time.waketime = args[4]; - ta->homing_start_time = args[4]; - ta->rest_ticks = args[5]; - ta->watchdog_max = args[6]; - ta->watchdog_count = 0; - ta->time.func = watchdog_event; - set_flag(FLAG_IS_HOMING, ta); - set_flag(FLAG_AWAIT_HOMING, ta); + ta->time.waketime = ta->homing_clock = args[4]; + ta->monitor_max = args[6]; + ta->monitor_count = 0; + ta->time.func = monitor_event; + ta->flags = TA_AWAIT_HOMING | TA_CAN_TRIGGER; sched_add_timer(&ta->time); } DECL_COMMAND(command_trigger_analog_home, "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); + " error_reason=%c clock=%u monitor_ticks=%u monitor_max=%u"); void command_trigger_analog_query_state(uint32_t *args) { uint8_t oid = args[0]; struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - sendf("trigger_analog_state oid=%c is_homing_trigger=%c trigger_ticks=%u" - , oid - , is_flag_set(FLAG_IS_HOMING_TRIGGER, ta) - , ta->trigger_ticks); + sendf("trigger_analog_state oid=%c homing=%c trigger_clock=%u" + , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->trigger_clock); } DECL_COMMAND(command_trigger_analog_query_state , "trigger_analog_query_state oid=%c"); diff --git a/src/trigger_analog.h b/src/trigger_analog.h index 9867095e8..53b36ce63 100644 --- a/src/trigger_analog.h +++ b/src/trigger_analog.h @@ -4,6 +4,7 @@ #include // uint8_t struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid); +void trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code); void trigger_analog_update(struct trigger_analog *ta, int32_t sample); #endif // trigger_analog.h From 58cc059e31d186797dc08e4cb35e5fae0d191bb5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 22:56:29 -0500 Subject: [PATCH 080/108] sensor_ldc1612: Convert homing code to use trigger_analog system Remove the homing code from sensor_ldc1612.c and utilize the generic homing support found in trigger_analog.c . Signed-off-by: Kevin O'Connor --- klippy/extras/ldc1612.py | 28 +++----- klippy/extras/probe_eddy_current.py | 54 +++++---------- src/Kconfig | 2 +- src/sensor_ldc1612.c | 102 ++++------------------------ 4 files changed, 40 insertions(+), 146 deletions(-) diff --git a/klippy/extras/ldc1612.py b/klippy/extras/ldc1612.py index db539c650..085dc2a55 100644 --- a/klippy/extras/ldc1612.py +++ b/klippy/extras/ldc1612.py @@ -87,7 +87,6 @@ class LDC1612: self._sensor_errors = {} self.oid = oid = mcu.create_oid() self.query_ldc1612_cmd = None - self.ldc1612_setup_home_cmd = self.query_ldc1612_home_state_cmd = None self.clock_freq = config.getint("frequency", DEFAULT_LDC1612_FREQ, 2000000, 40000000) # Coil frequency divider, assume 12MHz is BTT Eddy @@ -120,23 +119,22 @@ class LDC1612: hdr = ('time', 'frequency', 'z') self.batch_bulk.add_mux_endpoint("ldc1612/dump_ldc1612", "sensor", self.name, {'header': hdr}) + def setup_trigger_analog(self, trigger_analog_oid): + self.mcu.add_config_cmd( + "ldc1612_attach_trigger_analog oid=%d trigger_analog_oid=%d" + % (self.oid, trigger_analog_oid), is_init=True) def _build_config(self): cmdqueue = self.i2c.get_command_queue() self.query_ldc1612_cmd = self.mcu.lookup_command( "query_ldc1612 oid=%c rest_ticks=%u", cq=cmdqueue) self.ffreader.setup_query_command("query_status_ldc1612 oid=%c", oid=self.oid, cq=cmdqueue) - self.ldc1612_setup_home_cmd = self.mcu.lookup_command( - "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c error_reason=%c", cq=cmdqueue) - self.query_ldc1612_home_state_cmd = self.mcu.lookup_query_command( - "query_ldc1612_home_state oid=%c", - "ldc1612_home_state oid=%c homing=%c trigger_clock=%u", - oid=self.oid, cq=cmdqueue) errors = self.mcu.get_enumerations().get("ldc1612_error:", {}) self._sensor_errors = {v: k for k, v in errors.items()} def get_mcu(self): return self.i2c.get_mcu() + def get_samples_per_second(self): + return self.data_rate def read_reg(self, reg): if self.mcu.is_fileoutput(): return 0 @@ -148,20 +146,10 @@ class LDC1612: minclock=minclock) def add_client(self, cb): self.batch_bulk.add_client(cb) - # Homing - def setup_home(self, print_time, trigger_freq, - trsync_oid, hit_reason, err_reason): - clock = self.mcu.print_time_to_clock(print_time) - tfreq = int(trigger_freq / self.freq_conv + 0.5) - self.ldc1612_setup_home_cmd.send( - [self.oid, clock, tfreq, trsync_oid, hit_reason, err_reason]) - def clear_home(self): - self.ldc1612_setup_home_cmd.send([self.oid, 0, 0, 0, 0, 0]) - params = self.query_ldc1612_home_state_cmd.send([self.oid]) - tclock = self.mcu.clock32_to_clock64(params['trigger_clock']) - return self.mcu.clock_to_print_time(tclock) def lookup_sensor_error(self, error): return self._sensor_errors.get(error, "Unknown ldc1612 error") + def convert_frequency(self, freq): + return int(freq / self.freq_conv + 0.5) # Measurement decoding def _convert_samples(self, samples): freq_conv = self.freq_conv diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 4d97d46d6..d30c7d404 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math, bisect import mcu -from . import ldc1612, probe, manual_probe +from . import ldc1612, trigger_analog, probe, manual_probe OUT_OF_RANGE = 99.9 @@ -388,9 +388,10 @@ class EddyGatherSamples: self._probe_times.append((start_time, end_time, pos_time, None)) self._check_samples() +MAX_VALID_RAW_VALUE=0x03ffffff + # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: - REASON_SENSOR_ERROR = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, config, sensor_helper, calibration, probe_offsets, param_helper): self._printer = config.get_printer() @@ -399,43 +400,20 @@ class EddyDescend: self._calibration = calibration self._probe_offsets = probe_offsets self._param_helper = param_helper + self._trigger_analog = trigger_analog.MCU_trigger_analog(sensor_helper) self._z_min_position = probe.lookup_minimum_z(config) - self._dispatch = mcu.TriggerDispatch(self._mcu) - self._trigger_time = 0. self._gather = None - probe.LookupZSteppers(config, self._dispatch.add_stepper) - # Interface for phoming.probing_move() - def get_steppers(self): - return self._dispatch.get_steppers() - def home_start(self, print_time, sample_time, sample_count, rest_time, - triggered=True): - self._trigger_time = 0. + dispatch = self._trigger_analog.get_dispatch() + probe.LookupZSteppers(config, dispatch.add_stepper) + def _prep_trigger_analog(self): + self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) z_offset = self._probe_offsets.get_offsets()[2] trigger_freq = self._calibration.height_to_freq(z_offset) - trigger_completion = self._dispatch.start(print_time) - self._sensor_helper.setup_home( - print_time, trigger_freq, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_SENSOR_ERROR) - return trigger_completion - def home_wait(self, home_end_time): - self._dispatch.wait_end(home_end_time) - trigger_time = self._sensor_helper.clear_home() - res = self._dispatch.stop() - if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - raise self._printer.command_error( - "Communication timeout during homing") - error_code = res - self.REASON_SENSOR_ERROR - error_msg = self._sensor_helper.lookup_sensor_error(error_code) - raise self._printer.command_error(error_msg) - if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: - return 0. - if self._mcu.is_fileoutput(): - trigger_time = home_end_time - self._trigger_time = trigger_time - return trigger_time + conv_freq = self._sensor_helper.convert_frequency(trigger_freq) + self._trigger_analog.set_trigger('gt', conv_freq) # Probe session interface def start_probe_session(self, gcmd): + self._prep_trigger_analog() offsets = self._probe_offsets.get_offsets() self._gather = EddyGatherSamples(self._printer, self._sensor_helper, self._calibration, offsets) @@ -447,9 +425,9 @@ class EddyDescend: speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] # Perform probing move phoming = self._printer.lookup_object('homing') - trig_pos = phoming.probing_move(self, pos, speed) + trig_pos = phoming.probing_move(self._trigger_analog, pos, speed) # Extract samples - start_time = self._trigger_time + 0.050 + start_time = self._trigger_analog.get_last_trigger_time() + 0.050 end_time = start_time + 0.100 toolhead_pos = toolhead.get_position() self._gather.note_probe(start_time, end_time, toolhead_pos) @@ -472,13 +450,13 @@ class EddyEndstopWrapper: def add_stepper(self, stepper): pass def get_steppers(self): - return self._eddy_descend.get_steppers() + return self._eddy_descend._trigger_analog.get_steppers() def home_start(self, print_time, sample_time, sample_count, rest_time, triggered=True): - return self._eddy_descend.home_start( + return self._eddy_descend._trigger_analog.home_start( print_time, sample_time, sample_count, rest_time, triggered) def home_wait(self, home_end_time): - return self._eddy_descend.home_wait(home_end_time) + return self._eddy_descend._trigger_analog.home_wait(home_end_time) def query_endstop(self, print_time): return False # XXX # Interface for HomingViaProbeHelper diff --git a/src/Kconfig b/src/Kconfig index 8013b40f0..bf5149380 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -179,7 +179,7 @@ config NEED_SENSOR_BULK default y config WANT_TRIGGER_ANALOG bool - depends on WANT_HX71X || WANT_ADS1220 + depends on WANT_HX71X || WANT_ADS1220 || WANT_LDC1612 default y config NEED_SOS_FILTER bool diff --git a/src/sensor_ldc1612.c b/src/sensor_ldc1612.c index 35c69a2bd..2c794fafe 100644 --- a/src/sensor_ldc1612.c +++ b/src/sensor_ldc1612.c @@ -13,11 +13,10 @@ #include "i2ccmds.h" // i2cdev_oid_lookup #include "sched.h" // DECL_TASK #include "sensor_bulk.h" // sensor_bulk_report -#include "trsync.h" // trsync_do_trigger +#include "trigger_analog.h" // trigger_analog_update enum { LDC_PENDING = 1<<0, LDC_HAVE_INTB = 1<<1, - LH_AWAIT_HOMING = 1<<1, LH_CAN_TRIGGER = 1<<2 }; struct ldc1612 { @@ -27,12 +26,7 @@ struct ldc1612 { uint8_t flags; struct sensor_bulk sb; struct gpio_in intb_pin; - // homing - struct trsync *ts; - uint8_t homing_flags; - uint8_t trigger_reason, error_reason; - uint32_t trigger_threshold; - uint32_t homing_clock; + struct trigger_analog *ta; }; static struct task_wake ldc1612_wake; @@ -91,83 +85,15 @@ DECL_COMMAND(command_config_ldc1612_with_intb, "config_ldc1612_with_intb oid=%c i2c_oid=%c intb_pin=%c"); void -command_ldc1612_setup_home(uint32_t *args) -{ +ldc1612_attach_trigger_analog(uint32_t *args) { struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); - - ld->trigger_threshold = args[2]; - if (!ld->trigger_threshold) { - ld->ts = NULL; - ld->homing_flags = 0; - return; - } - ld->homing_clock = args[1]; - ld->ts = trsync_oid_lookup(args[3]); - ld->trigger_reason = args[4]; - ld->error_reason = args[5]; - ld->homing_flags = LH_AWAIT_HOMING | LH_CAN_TRIGGER; + ld->ta = trigger_analog_oid_lookup(args[1]); } -DECL_COMMAND(command_ldc1612_setup_home, - "ldc1612_setup_home oid=%c clock=%u threshold=%u" - " trsync_oid=%c trigger_reason=%c error_reason=%c"); +DECL_COMMAND(ldc1612_attach_trigger_analog, + "ldc1612_attach_trigger_analog oid=%c trigger_analog_oid=%c"); -void -command_query_ldc1612_home_state(uint32_t *args) -{ - struct ldc1612 *ld = oid_lookup(args[0], command_config_ldc1612); - sendf("ldc1612_home_state oid=%c homing=%c trigger_clock=%u" - , args[0], !!(ld->homing_flags & LH_CAN_TRIGGER), ld->homing_clock); -} -DECL_COMMAND(command_query_ldc1612_home_state, - "query_ldc1612_home_state oid=%c"); - -// Cancel homing due to an error -static void -cancel_homing(struct ldc1612 *ld, int error_code) -{ - if (!(ld->homing_flags & LH_CAN_TRIGGER)) - return; - ld->homing_flags = 0; - trsync_do_trigger(ld->ts, ld->error_reason + error_code); -} - -#define MAX_VALID_RAW_VALUE 0x03ffffff #define DATA_ERROR_AMPLITUDE (1L << 28) -static int -check_data_bits(struct ldc1612 *ld, uint32_t raw_data) { - // Ignore amplitude errors - raw_data &= ~DATA_ERROR_AMPLITUDE; - // Datasheet define valid frequency input as < F_ref / 4 - if (raw_data < MAX_VALID_RAW_VALUE) - return 0; - cancel_homing(ld, SE_SENSOR_ERROR); - return -1; -} - -// Check if a sample should trigger a homing event -static void -check_home(struct ldc1612 *ld, uint32_t raw_data) -{ - uint8_t homing_flags = ld->homing_flags; - if (!(homing_flags & LH_CAN_TRIGGER)) - return; - if (check_data_bits(ld, raw_data)) - return; - uint32_t data = raw_data & 0x0fffffff; - uint32_t time = timer_read_time(); - if ((homing_flags & LH_AWAIT_HOMING) - && timer_is_before(time, ld->homing_clock)) - return; - homing_flags &= ~LH_AWAIT_HOMING; - if (data > ld->trigger_threshold) { - homing_flags = 0; - ld->homing_clock = time; - trsync_do_trigger(ld->ts, ld->trigger_reason); - } - ld->homing_flags = homing_flags; -} - // Chip registers #define REG_DATA0_MSB 0x00 #define REG_DATA0_LSB 0x01 @@ -196,7 +122,7 @@ read_reg_status(struct ldc1612 *ld, uint16_t *status) static void report_sample_error(struct ldc1612 *ld, int error_code) { - cancel_homing(ld, error_code); + trigger_analog_note_error(ld->ta, error_code); uint8_t *d = &ld->sb.data[ld->sb.data_count]; d[0] = 0xff; @@ -238,12 +164,14 @@ ldc1612_query(struct ldc1612 *ld, uint8_t oid) goto out; } - // Check for endstop trigger - uint32_t data = ((uint32_t)d[0] << 24) - | ((uint32_t)d[1] << 16) - | ((uint32_t)d[2] << 8) - | ((uint32_t)d[3]); - check_home(ld, data); + // Check for homing trigger + uint32_t raw_data = (((uint32_t)d[0] << 24) | ((uint32_t)d[1] << 16) + | ((uint32_t)d[2] << 8) | ((uint32_t)d[3])); + if (raw_data & 0xe0000000) + trigger_analog_note_error(ld->ta, SE_SENSOR_ERROR); + else + trigger_analog_update(ld->ta, raw_data & 0x0fffffff); + out: ld->sb.data_count += BYTES_PER_SAMPLE; // Flush local buffer if needed From 16755481ccd0f08e24e1a552e6b3e961a2c906e8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 23 Jan 2026 18:38:12 -0500 Subject: [PATCH 081/108] trigger_analog: Support scaling the filter's initial start state Add a new set_start_state() method to MCU_SosFilter that can arrange for the filter to better handle a non-zero initial starting state. Also, this removes the previous 1.0 gram initial start state for load cells as it tares the initial value. Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 2a6952ccb..133eabea7 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -82,6 +82,7 @@ class MCU_SosFilter: self._coeff_frac_bits = coeff_frac_bits self._value_frac_bits = self._scale_frac_bits = 0 self._design = None + self._start_value = 0. self._offset = 0 self._scale = 1 self._set_section_cmd = self._set_state_cmd = None @@ -154,10 +155,16 @@ class MCU_SosFilter: % (nun_states,)) fixed_state = [] for col, value in enumerate(section): - fixed_state.append(to_fixed_32(value, self._value_frac_bits)) + adjval = value * self._start_value + fixed_state.append(to_fixed_32(adjval, self._value_frac_bits)) sos_state.append(fixed_state) return sos_state + # Set expected state when filter first starts (avoids filter + # "ringing" if sensor data has a known static offset) + def set_start_state(self, start_value): + self._start_value = start_value + # Set conversion of a raw value 1 to a 1.0 value processed by sos filter def set_offset_scale(self, offset, scale, scale_frac_bits=0, value_frac_bits=0): From 36b98981176a2429cbf1d811ac71f8a79f1588b1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 24 Jan 2026 18:39:14 -0500 Subject: [PATCH 082/108] trigger_analog: Set the MCU_SosFilter coeff_frac_bits in set_filter_design() This parameter is rarely changed and is directly tied to the coefficients in the filter "design". Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 3 +-- klippy/extras/trigger_analog.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index 5f2a3c111..f36d46124 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -486,8 +486,7 @@ class LoadCellPrinterProbe: self._mcu = self._load_cell.get_sensor().get_mcu() self._mcu_trigger_analog = trigger_analog.MCU_trigger_analog(sensor) cmd_queue = self._mcu_trigger_analog.get_dispatch().get_command_queue() - sos_filter = trigger_analog.MCU_SosFilter(self._mcu, cmd_queue, 4, - Q2_29_FRAC_BITS) + sos_filter = trigger_analog.MCU_SosFilter(self._mcu, cmd_queue, 4) self._mcu_trigger_analog.setup_sos_filter(sos_filter) continuous_tare_filter_helper = ContinuousTareFilterHelper( config, sensor, sos_filter) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 133eabea7..279e542f1 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -74,17 +74,20 @@ class DigitalFilter: class MCU_SosFilter: # max_sections should be the largest number of sections you expect # to use at runtime. - def __init__(self, mcu, cmd_queue, max_sections, coeff_frac_bits=29): + def __init__(self, mcu, cmd_queue, max_sections): self._mcu = mcu self._cmd_queue = cmd_queue - self._oid = self._mcu.create_oid() self._max_sections = max_sections - self._coeff_frac_bits = coeff_frac_bits - self._value_frac_bits = self._scale_frac_bits = 0 + # SOS filter "design" self._design = None + self._coeff_frac_bits = 0 self._start_value = 0. + # Offset and scaling self._offset = 0 self._scale = 1 + self._value_frac_bits = self._scale_frac_bits = 0 + # MCU commands + self._oid = self._mcu.create_oid() self._set_section_cmd = self._set_state_cmd = None self._set_active_cmd = self._set_offset_scale_cmd = None self._last_sent_coeffs = [None] * self._max_sections @@ -175,8 +178,9 @@ class MCU_SosFilter: self._scale_frac_bits = scale_frac_bits # Change the filter coefficients and state at runtime - def set_filter_design(self, design): + def set_filter_design(self, design, coeff_frac_bits=29): self._design = design + self._coeff_frac_bits = coeff_frac_bits # Resets the filter state back to initial conditions at runtime def reset_filter(self): @@ -251,7 +255,7 @@ class MCU_trigger_analog: self._sensor.setup_trigger_analog(self._oid) cmd_queue = self._dispatch.get_command_queue() if self._sos_filter is None: - self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0, 0)) + self.setup_sos_filter(MCU_SosFilter(self._mcu, cmd_queue, 0)) self._mcu.add_config_cmd( "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) From 30720d29b51c5c70de732b24e180c0a9233b775c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 24 Jan 2026 20:05:12 -0500 Subject: [PATCH 083/108] trigger_analog: Avoid storing value_frac_bits in MCU_SosFilter It is simpler for callers to convert values to their desired format. Signed-off-by: Kevin O'Connor --- klippy/extras/load_cell_probe.py | 22 +++++++++++----------- klippy/extras/trigger_analog.py | 30 +++++++++--------------------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index f36d46124..1e361d5d1 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -9,9 +9,8 @@ from . import probe, trigger_analog, load_cell, hx71x, ads1220 np = None # delay NumPy import until configuration time -# constants for fixed point numbers -Q2_29_FRAC_BITS = 29 -Q16_15_FRAC_BITS = 15 +# MCU SOS filter scaled to "fractional grams" for consistent sensor precision +FRAC_GRAMS_CONV = 32768.0 class TapAnalysis: @@ -272,9 +271,9 @@ class LoadCellProbeConfigHelper: def get_grams_per_count(self): counts_per_gram = self._load_cell.get_counts_per_gram() # The counts_per_gram could be so large that it becomes 0.0 when - # converted to Q2.29 format. This would mean the ADC range only measures + # sent to the mcu. This would mean the ADC range only measures # a few grams which seems very unlikely. Treat this as an error: - if counts_per_gram >= 2**Q2_29_FRAC_BITS: + if counts_per_gram >= (1<<29): raise OverflowError("counts_per_gram value is too large to filter") return 1. / counts_per_gram @@ -318,17 +317,18 @@ class LoadCellProbingMove: self._continuous_tare_filter_helper.update_from_command(gcmd) # update the load cell so it reflects the new tare value self._load_cell.tare(tare_counts) - # update range and trigger + # update raw range safety_min, safety_max = self._config_helper.get_safety_range(gcmd) self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) - trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - self._mcu_trigger_analog.set_trigger("abs_ge", trigger_val) # update internal tare value - gpc = self._config_helper.get_grams_per_count() + gpc = self._config_helper.get_grams_per_count() * FRAC_GRAMS_CONV sos_filter = self._mcu_trigger_analog.get_sos_filter() Q17_14_FRAC_BITS = 14 - sos_filter.set_offset_scale(int(-tare_counts), gpc, - Q17_14_FRAC_BITS, Q16_15_FRAC_BITS) + sos_filter.set_offset_scale(int(-tare_counts), gpc, Q17_14_FRAC_BITS) + # update trigger + trigger_val = self._config_helper.get_trigger_force_grams(gcmd) + trigger_frac_grams = trigger_val * FRAC_GRAMS_CONV + self._mcu_trigger_analog.set_trigger("abs_ge", trigger_frac_grams) # Probe towards z_min until the trigger_analog on the MCU triggers def probing_move(self, gcmd): diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 279e542f1..27af6777e 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -21,7 +21,7 @@ def assert_is_int32(value, frac_bits): # convert a floating point value to a 32 bit fixed point representation # checks for overflow -def to_fixed_32(value, frac_bits): +def to_fixed_32(value, frac_bits=0): fixed_val = int(value * (2**frac_bits)) return assert_is_int32(fixed_val, frac_bits) @@ -84,8 +84,8 @@ class MCU_SosFilter: self._start_value = 0. # Offset and scaling self._offset = 0 - self._scale = 1 - self._value_frac_bits = self._scale_frac_bits = 0 + self._scale = 1. + self._scale_frac_bits = 0 # MCU commands self._oid = self._mcu.create_oid() self._set_section_cmd = self._set_state_cmd = None @@ -96,12 +96,6 @@ class MCU_SosFilter: % (self._oid, self._max_sections)) self._mcu.register_config_callback(self._build_config) - def _validate_frac_bits(self, frac_bits): - if frac_bits < 0 or frac_bits > 31: - raise ValueError("The number of fractional bits (%i) must be a" - " value between 0 and 31" % (frac_bits,)) - return frac_bits - def _build_config(self): self._set_section_cmd = self._mcu.lookup_command( "sos_filter_set_section oid=%c section_idx=%c" @@ -119,9 +113,6 @@ class MCU_SosFilter: def get_oid(self): return self._oid - def convert_value(self, val): - return to_fixed_32(val, self._value_frac_bits) - # convert the SciPi SOS filters to fixed point format def _convert_filter(self): if self._design is None: @@ -159,7 +150,7 @@ class MCU_SosFilter: fixed_state = [] for col, value in enumerate(section): adjval = value * self._start_value - fixed_state.append(to_fixed_32(adjval, self._value_frac_bits)) + fixed_state.append(to_fixed_32(adjval)) sos_state.append(fixed_state) return sos_state @@ -169,12 +160,9 @@ class MCU_SosFilter: self._start_value = start_value # Set conversion of a raw value 1 to a 1.0 value processed by sos filter - def set_offset_scale(self, offset, scale, scale_frac_bits=0, - value_frac_bits=0): + def set_offset_scale(self, offset, scale, scale_frac_bits=0): self._offset = offset - self._value_frac_bits = value_frac_bits - scale_mult = scale * float(1 << value_frac_bits) - self._scale = to_fixed_32(scale_mult, scale_frac_bits) + self._scale = scale self._scale_frac_bits = scale_frac_bits # Change the filter coefficients and state at runtime @@ -207,7 +195,8 @@ class MCU_SosFilter: for i, state in enumerate(sos_state): self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Send offset/scale (if they have changed) - args = (self._oid, self._offset, self._scale, self._scale_frac_bits) + su = to_fixed_32(self._scale, self._scale_frac_bits) + args = (self._oid, self._offset, su, self._scale_frac_bits) if args != self._last_sent_offset_scale: self._set_offset_scale_cmd.send(args) self._last_sent_offset_scale = args @@ -309,8 +298,7 @@ class MCU_trigger_analog: self._set_raw_range_cmd.send(args) self._last_range_args = args # Update trigger in mcu (if it has changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._trigger_type, tval32] + args = [self._oid, self._trigger_type, to_fixed_32(self._trigger_value)] if args != self._last_trigger_args: self._set_trigger_cmd.send(args) self._last_trigger_args = args From 9a97ade74f78dedbd0a3fe048004a4dfb9e3e2c1 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 24 Jan 2026 12:12:39 -0500 Subject: [PATCH 084/108] sos_filter: Implement auto_offset feature Add a setting that will enable the mcu sos_filter code to automatically set an offset using the first read measurement. Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 12 ++++++++---- src/sos_filter.c | 11 ++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 27af6777e..f053611d8 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -86,6 +86,7 @@ class MCU_SosFilter: self._offset = 0 self._scale = 1. self._scale_frac_bits = 0 + self._auto_offset = False # MCU commands self._oid = self._mcu.create_oid() self._set_section_cmd = self._set_state_cmd = None @@ -105,7 +106,7 @@ class MCU_SosFilter: cq=self._cmd_queue) self._set_offset_scale_cmd = self._mcu.lookup_command( "sos_filter_set_offset_scale oid=%c offset=%i" - " scale=%i scale_frac_bits=%c", cq=self._cmd_queue) + " scale=%i scale_frac_bits=%c auto_offset=%c", cq=self._cmd_queue) self._set_active_cmd = self._mcu.lookup_command( "sos_filter_set_active oid=%c n_sections=%c coeff_frac_bits=%c", cq=self._cmd_queue) @@ -160,10 +161,12 @@ class MCU_SosFilter: self._start_value = start_value # Set conversion of a raw value 1 to a 1.0 value processed by sos filter - def set_offset_scale(self, offset, scale, scale_frac_bits=0): + def set_offset_scale(self, offset=0, scale=1., scale_frac_bits=0, + auto_offset=False): self._offset = offset self._scale = scale self._scale_frac_bits = scale_frac_bits + self._auto_offset = auto_offset # Change the filter coefficients and state at runtime def set_filter_design(self, design, coeff_frac_bits=29): @@ -196,8 +199,9 @@ class MCU_SosFilter: self._set_state_cmd.send([self._oid, i, state[0], state[1]]) # Send offset/scale (if they have changed) su = to_fixed_32(self._scale, self._scale_frac_bits) - args = (self._oid, self._offset, su, self._scale_frac_bits) - if args != self._last_sent_offset_scale: + args = (self._oid, self._offset, su, self._scale_frac_bits, + self._auto_offset) + if args != self._last_sent_offset_scale or self._auto_offset: self._set_offset_scale_cmd.send(args) self._last_sent_offset_scale = args # Activate filter diff --git a/src/sos_filter.c b/src/sos_filter.c index c11ae3df8..00e8d9b09 100644 --- a/src/sos_filter.c +++ b/src/sos_filter.c @@ -21,6 +21,7 @@ struct sos_filter_section { struct sos_filter { uint8_t max_sections, n_sections, coeff_frac_bits, scale_frac_bits; + uint8_t auto_offset; int32_t offset, scale; // filter composed of second order sections struct sos_filter_section filter[0]; @@ -58,6 +59,12 @@ sos_filter_apply(struct sos_filter *sf, int32_t *pvalue) { int32_t raw_val = *pvalue; + // Automatically apply offset (if requested) + if (sf->auto_offset) { + sf->offset = -raw_val; + sf->auto_offset = 0; + } + // Apply offset and scale int32_t offset = sf->offset, offset_val = raw_val + offset, cur_val; if ((offset >= 0) != (offset_val >= raw_val)) @@ -163,9 +170,11 @@ command_trigger_analog_set_offset_scale(uint32_t *args) sf->offset = args[1]; sf->scale = args[2]; sf->scale_frac_bits = args[3] & 0x3f; + sf->auto_offset = args[4]; } DECL_COMMAND(command_trigger_analog_set_offset_scale, - "sos_filter_set_offset_scale oid=%c offset=%i scale=%i scale_frac_bits=%c"); + "sos_filter_set_offset_scale oid=%c offset=%i scale=%i scale_frac_bits=%c" + " auto_offset=%c"); // Set one section of the filter void From c9d904aa9d9de3ddb84a4c929f9d667c085329fc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 11:40:40 -0500 Subject: [PATCH 085/108] trigger_analog: Add initial support for detecting "tap" events Add a new "diff_peak_gt" trigger type. This will be useful with detecting ldc1612 "tap" events. Signed-off-by: Kevin O'Connor --- src/trigger_analog.c | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/trigger_analog.c b/src/trigger_analog.c index f7ec477ec..a77af68d7 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -22,12 +22,13 @@ struct trigger_analog { // Filtering struct sos_filter *sf; // Trigger value checking - int32_t trigger_value; + int32_t trigger_value, trigger_peak; + uint32_t trigger_clock; uint8_t trigger_type; // Trsync triggering uint8_t flags, trigger_reason, error_reason; struct trsync *ts; - uint32_t homing_clock, trigger_clock; + uint32_t homing_clock; // Sensor activity monitoring uint8_t monitor_max, monitor_count; struct timer time; @@ -41,10 +42,11 @@ enum { // Trigger types enum { - TT_ABS_GE, TT_GT + TT_ABS_GE, TT_GT, TT_DIFF_PEAK_GT }; DECL_ENUMERATION("trigger_analog_type", "abs_ge", TT_ABS_GE); DECL_ENUMERATION("trigger_analog_type", "gt", TT_GT); +DECL_ENUMERATION("trigger_analog_type", "diff_peak_gt", TT_DIFF_PEAK_GT); // Sample errors sent via trsync error code enum { @@ -85,17 +87,34 @@ monitor_note_activity(struct trigger_analog *ta) // Check if a value should signal a "trigger" event static int -check_trigger(struct trigger_analog *ta, int32_t value) +check_trigger(struct trigger_analog *ta, uint32_t time, int32_t value) { switch (ta->trigger_type) { case TT_ABS_GE: + ta->trigger_clock = time; return abs(value) >= ta->trigger_value; case TT_GT: + ta->trigger_clock = time; return value > ta->trigger_value; + case TT_DIFF_PEAK_GT: + if (value > ta->trigger_peak) { + ta->trigger_clock = time; + ta->trigger_peak = value; + return 0; + } + uint32_t delta = ta->trigger_peak - value; + return delta > ta->trigger_value; } return 0; } +// Reset fields associated with trigger checking +static void +trigger_reset(struct trigger_analog *ta) +{ + ta->trigger_peak = INT32_MIN; +} + // Stop homing due to an error static void cancel_homing(struct trigger_analog *ta, uint8_t error_code) @@ -150,10 +169,9 @@ trigger_analog_update(struct trigger_analog *ta, int32_t sample) } // Check if this is a "trigger" - ret = check_trigger(ta, filtered_value); + ret = check_trigger(ta, time, filtered_value); if (ret) { trsync_do_trigger(ta->ts, ta->trigger_reason); - ta->trigger_clock = time; flags = 0; } @@ -220,6 +238,7 @@ command_trigger_analog_home(uint32_t *args) ta->monitor_count = 0; ta->time.func = monitor_event; ta->flags = TA_AWAIT_HOMING | TA_CAN_TRIGGER; + trigger_reset(ta); sched_add_timer(&ta->time); } DECL_COMMAND(command_trigger_analog_home, From 7e394d6dd7cf1348342ff265385eb3afea4f1c59 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 18:30:18 -0500 Subject: [PATCH 086/108] bed_mesh: Fix tuple vs list error Commit 2e0c2262e incorrectly changed the internal fpt variable from a list to a tuple. Reported by @nefelim4ag. Signed-off-by: Kevin O'Connor --- klippy/extras/bed_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 9e5b206e9..dbe4f331c 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -682,7 +682,7 @@ class BedMeshCalibrate: idx_offset = 0 start_idx = 0 for i, pts in substitutes.items(): - fpt = base_points[i][:2] + fpt = list(base_points[i][:2]) # offset the index to account for additional samples idx = i + idx_offset # Add "normal" points From 4d9d57c0bd811e63bc911f6fad4ce039348aa2be Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 18:38:39 -0500 Subject: [PATCH 087/108] test: Add a bed_mesh test case Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- test/klippy/bed_mesh.cfg | 81 +++++++++++++++++++++++++++++++++++++++ test/klippy/bed_mesh.test | 13 +++++++ 2 files changed, 94 insertions(+) create mode 100644 test/klippy/bed_mesh.cfg create mode 100644 test/klippy/bed_mesh.test diff --git a/test/klippy/bed_mesh.cfg b/test/klippy/bed_mesh.cfg new file mode 100644 index 000000000..a3a56d92a --- /dev/null +++ b/test/klippy/bed_mesh.cfg @@ -0,0 +1,81 @@ +# Test config for bed_mesh +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: probe:z_virtual_endstop +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[heater_bed] +heater_pin: PH5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +control: watermark +min_temp: 0 +max_temp: 130 + +[probe] +pin: PC7 +z_offset: 1.15 + +[bed_mesh] +mesh_min: 10,10 +mesh_max: 180,180 +probe_count: 7, 7 +algorithm: bicubic +faulty_region_1_min: 21.422, 87.126 +faulty_region_1_max: 42.922, 129.126 +faulty_region_2_min: 54.172, 97.376 +faulty_region_2_max: 100.172, 150.876 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/bed_mesh.test b/test/klippy/bed_mesh.test new file mode 100644 index 000000000..bf104755d --- /dev/null +++ b/test/klippy/bed_mesh.test @@ -0,0 +1,13 @@ +# Test case for bed_mesh tests +CONFIG bed_mesh.cfg +DICTIONARY atmega2560.dict + +# Start by homing the printer. +G28 +G1 F6000 +G1 X60 Y60 Z10 + +# Run bed_mesh_calibrate +BED_MESH_CALIBRATE + +G1 Z10 From 7f822f3a5c27743b3e49a8e0d7b9d81bc3176f17 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 27 Jan 2026 01:03:55 +0100 Subject: [PATCH 088/108] probe: fix sign inversion in probe calibrate (#7178) Commit 2a1027ce inadvertently flipped the signs in probe_calibrate_finalize(). Signed-off-by: Timofey Titovets --- klippy/extras/probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 31e5b71e3..73fd8927b 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -95,7 +95,7 @@ class ProbeCommandHelper: if mpresult is None: return ppos, offsets = self.probe_calibrate_info - z_offset = offsets[2] + mpresult.bed_z - ppos.bed_z + z_offset = offsets[2] - mpresult.bed_z + ppos.bed_z gcode = self.printer.lookup_object('gcode') gcode.respond_info( "%s: z_offset: %.3f\n" From 9957546ae0ebd10b0fc27de0950bd0b01341f082 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 10:26:38 -0500 Subject: [PATCH 089/108] probe_eddy_current: Rework EddyGatherSamples() Rework the internal EddyGatherSamples() class with a goal of making it easier to add tap analysis in the future. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 217 +++++++++++++++------------- 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index d30c7d404..3668cd190 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -34,8 +34,12 @@ class EddyCalibration: gcode.register_command('Z_OFFSET_APPLY_PROBE', self.cmd_Z_OFFSET_APPLY_PROBE, desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) - def is_calibrated(self): - return len(self.cal_freqs) > 2 + def get_printer(self): + return self.printer + def verify_calibrated(self): + if len(self.cal_freqs) <= 2: + raise self.printer.command_error( + "Must calibrate probe_eddy_current first") def load_calibration(self, cal): cal = sorted([(c[1], c[0]) for c in cal]) self.cal_freqs = [c[0] for c in cal] @@ -278,56 +282,33 @@ class EddyCalibration: # Tool to gather samples and convert them to probe positions class EddyGatherSamples: - def __init__(self, printer, sensor_helper, calibration, offsets): + def __init__(self, printer, sensor_helper): self._printer = printer self._sensor_helper = sensor_helper - self._calibration = calibration - self._offsets = offsets - # Results storage - self._samples = [] - self._probe_times = [] - self._probe_results = [] + # Sensor reading + self._sensor_messages = [] self._need_stop = False + # Probe request and results storage + self._probe_requests = [] + self._analysis_results = [] # Start samples - if not self._calibration.is_calibrated(): - raise self._printer.command_error( - "Must calibrate probe_eddy_current first") - sensor_helper.add_client(self._add_measurement) - def _add_measurement(self, msg): + sensor_helper.add_client(self._add_sensor_message) + # Sensor reading and measurement extraction + def _add_sensor_message(self, msg): if self._need_stop: - del self._samples[:] + del self._sensor_messages[:] return False - self._samples.append(msg) - self._check_samples() + self._sensor_messages.append(msg) + self._check_sensor_messages() return True def finish(self): self._need_stop = True - def _await_samples(self): - # Make sure enough samples have been collected - reactor = self._printer.get_reactor() - mcu = self._sensor_helper.get_mcu() - while self._probe_times: - start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] - systime = reactor.monotonic() - est_print_time = mcu.estimated_print_time(systime) - if est_print_time > end_time + 1.0: - raise self._printer.command_error( - "probe_eddy_current sensor outage") - if mcu.is_fileoutput(): - # In debugging mode - if pos_time is not None: - toolhead_pos = self._lookup_toolhead_pos(pos_time) - self._probe_results.append((toolhead_pos[2], toolhead_pos)) - self._probe_times.pop(0) - continue - reactor.pause(systime + 0.010) - def _pull_freq(self, start_time, end_time): - # Find average sensor frequency between time range + def _pull_measurements(self, start_time, end_time): + # Extract measurements from sensor messages for given time range + measures = [] msg_num = discard_msgs = 0 - samp_sum = 0. - samp_count = 0 - while msg_num < len(self._samples): - msg = self._samples[msg_num] + while msg_num < len(self._sensor_messages): + msg = self._sensor_messages[msg_num] msg_num += 1 data = msg['data'] if data[0][0] > end_time: @@ -335,58 +316,78 @@ class EddyGatherSamples: if data[-1][0] < start_time: discard_msgs = msg_num continue - for time, freq, z in data: - if time >= start_time and time <= end_time: - samp_sum += freq - samp_count += 1 - del self._samples[:discard_msgs] - if not samp_count: - # No sensor readings - raise error in pull_probed() - return 0. - return samp_sum / samp_count - def _lookup_toolhead_pos(self, pos_time): - toolhead = self._printer.lookup_object('toolhead') - kin = toolhead.get_kinematics() - kin_spos = {s.get_name(): s.mcu_to_commanded_position( - s.get_past_mcu_position(pos_time)) - for s in kin.get_steppers()} - return kin.calc_position(kin_spos) - def _check_samples(self): - while self._samples and self._probe_times: - start_time, end_time, pos_time, toolhead_pos = self._probe_times[0] - if self._samples[-1]['data'][-1][0] < end_time: + for measure in data: + time = measure[0] + if time < start_time: + continue + if time > end_time: + break + measures.append(measure) + del self._sensor_messages[:discard_msgs] + return measures + def _check_sensor_messages(self): + while self._sensor_messages and self._probe_requests: + cb, start_time, end_time, args = self._probe_requests[0] + if self._sensor_messages[-1]['data'][-1][0] < end_time: break - freq = self._pull_freq(start_time, end_time) - if pos_time is not None: - toolhead_pos = self._lookup_toolhead_pos(pos_time) - sensor_z = None - if freq: - sensor_z = self._calibration.freq_to_height(freq) - self._probe_results.append((sensor_z, toolhead_pos)) - self._probe_times.pop(0) + measures = self._pull_measurements(start_time, end_time) + errmsg = res = None + try: + # Call analysis callback to process measurements + res = cb(measures, *args) + except self._printer.command_error as e: + # Defer raising of errors to pull_probed() + errmsg = str(e) + self._analysis_results.append((res, errmsg)) + self._probe_requests.pop(0) + def add_probe_request(self, cb, start_time, end_time, *args): + self._probe_requests.append((cb, start_time, end_time, args)) + self._check_sensor_messages() + # Extract probe results + def _await_sensor_messages(self): + # Make sure enough samples have been collected + reactor = self._printer.get_reactor() + mcu = self._sensor_helper.get_mcu() + while self._probe_requests: + cb, start_time, end_time, args = self._probe_requests[0] + systime = reactor.monotonic() + est_print_time = mcu.estimated_print_time(systime) + if est_print_time > end_time + 1.0: + raise self._printer.command_error( + "probe_eddy_current sensor outage") + if mcu.is_fileoutput(): + # In debugging mode - just create dummy response + dummy_pr = manual_probe.ProbeResult(0., 0., 0., 0., 0., 0.) + self._analysis_results.append((dummy_pr, None)) + self._probe_requests.pop(0) + continue + reactor.pause(systime + 0.010) def pull_probed(self): - self._await_samples() + self._await_sensor_messages() results = [] - for sensor_z, toolhead_pos in self._probe_results: - if sensor_z is None: - raise self._printer.command_error( - "Unable to obtain probe_eddy_current sensor readings") - if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: - raise self._printer.command_error( - "probe_eddy_current sensor not in valid range") - res = manual_probe.ProbeResult( - toolhead_pos[0]+self._offsets[0], - toolhead_pos[1]+self._offsets[1], toolhead_pos[2]-sensor_z, - toolhead_pos[0], toolhead_pos[1], toolhead_pos[2]) + for res, errmsg in self._analysis_results: + if errmsg is not None: + raise self._printer.command_error(errmsg) results.append(res) - del self._probe_results[:] + del self._analysis_results[:] return results - def note_probe(self, start_time, end_time, toolhead_pos): - self._probe_times.append((start_time, end_time, None, toolhead_pos)) - self._check_samples() - def note_probe_and_position(self, start_time, end_time, pos_time): - self._probe_times.append((start_time, end_time, pos_time, None)) - self._check_samples() + +# Generate a ProbeResult from the average of a set of measurements +def probe_results_from_avg(measures, toolhead_pos, calibration, offsets): + cmderr = calibration.get_printer().command_error + if not measures: + raise cmderr("Unable to obtain probe_eddy_current sensor readings") + # Determine average of measurements + freq_sum = sum([m[1] for m in measures]) + freq_avg = freq_sum / len(measures) + # Determine height associated with frequency + sensor_z = calibration.freq_to_height(freq_avg) + if sensor_z <= -OUT_OF_RANGE or sensor_z >= OUT_OF_RANGE: + raise cmderr("probe_eddy_current sensor not in valid range") + return manual_probe.ProbeResult( + toolhead_pos[0] + offsets[0], toolhead_pos[1] + offsets[1], + toolhead_pos[2] - sensor_z, + toolhead_pos[0], toolhead_pos[1], toolhead_pos[2]) MAX_VALID_RAW_VALUE=0x03ffffff @@ -396,7 +397,6 @@ class EddyDescend: probe_offsets, param_helper): self._printer = config.get_printer() self._sensor_helper = sensor_helper - self._mcu = sensor_helper.get_mcu() self._calibration = calibration self._probe_offsets = probe_offsets self._param_helper = param_helper @@ -413,10 +413,9 @@ class EddyDescend: self._trigger_analog.set_trigger('gt', conv_freq) # Probe session interface def start_probe_session(self, gcmd): + self._calibration.verify_calibrated() self._prep_trigger_analog() - offsets = self._probe_offsets.get_offsets() - self._gather = EddyGatherSamples(self._printer, self._sensor_helper, - self._calibration, offsets) + self._gather = EddyGatherSamples(self._printer, self._sensor_helper) return self def run_probe(self, gcmd): toolhead = self._printer.lookup_object('toolhead') @@ -430,7 +429,10 @@ class EddyDescend: start_time = self._trigger_analog.get_last_trigger_time() + 0.050 end_time = start_time + 0.100 toolhead_pos = toolhead.get_position() - self._gather.note_probe(start_time, end_time, toolhead_pos) + offsets = self._probe_offsets.get_offsets() + self._gather.add_probe_request(probe_results_from_avg, + start_time, end_time, + toolhead_pos, self._calibration, offsets) def pull_probed_results(self): return self._gather.pull_probed() def end_probe_session(self): @@ -477,19 +479,31 @@ class EddyEndstopWrapper: class EddyScanningProbe: def __init__(self, printer, sensor_helper, calibration, probe_offsets, gcmd): + calibration.verify_calibrated() self._printer = printer self._sensor_helper = sensor_helper self._calibration = calibration - offsets = probe_offsets.get_offsets() - self._gather = EddyGatherSamples(printer, sensor_helper, - calibration, offsets) + self._offsets = probe_offsets.get_offsets() + self._gather = EddyGatherSamples(printer, sensor_helper) self._sample_time_delay = 0.050 self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + def _lookup_toolhead_pos(self, pos_time): + toolhead = self._printer.lookup_object('toolhead') + kin = toolhead.get_kinematics() + kin_spos = {s.get_name(): s.mcu_to_commanded_position( + s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers()} + return kin.calc_position(kin_spos) + def _analyze_scan(self, measures, pos_time): + toolhead_pos = self._lookup_toolhead_pos(pos_time) + return probe_results_from_avg(measures, toolhead_pos, + self._calibration, self._offsets) def _rapid_lookahead_cb(self, printtime): start_time = printtime - self._sample_time / 2 - self._gather.note_probe_and_position( - start_time, start_time + self._sample_time, printtime) + end_time = start_time + self._sample_time + self._gather.add_probe_request(self._analyze_scan, start_time, end_time, + printtime) def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") if self._is_rapid: @@ -498,8 +512,9 @@ class EddyScanningProbe: printtime = toolhead.get_last_move_time() toolhead.dwell(self._sample_time_delay + self._sample_time) start_time = printtime + self._sample_time_delay - self._gather.note_probe_and_position( - start_time, start_time + self._sample_time, start_time) + end_time = start_time + self._sample_time + self._gather.add_probe_request(self._analyze_scan, start_time, end_time, + start_time) def pull_probed_results(self): if self._is_rapid: # Flush lookahead (so all lookahead callbacks are invoked) From 8922481034c9c767610d5577711a6cd9fc18b183 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 14:04:14 -0500 Subject: [PATCH 090/108] probe_eddy_current: Create trigger_analog instance in main PrinterEddyProbe Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 3668cd190..909a6cbcc 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -394,17 +394,15 @@ MAX_VALID_RAW_VALUE=0x03ffffff # Helper for implementing PROBE style commands (descend until trigger) class EddyDescend: def __init__(self, config, sensor_helper, calibration, - probe_offsets, param_helper): + probe_offsets, param_helper, trigger_analog): self._printer = config.get_printer() self._sensor_helper = sensor_helper self._calibration = calibration self._probe_offsets = probe_offsets self._param_helper = param_helper - self._trigger_analog = trigger_analog.MCU_trigger_analog(sensor_helper) + self._trigger_analog = trigger_analog self._z_min_position = probe.lookup_minimum_z(config) self._gather = None - dispatch = self._trigger_analog.get_dispatch() - probe.LookupZSteppers(config, dispatch.add_stepper) def _prep_trigger_analog(self): self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) z_offset = self._probe_offsets.get_offsets()[2] @@ -538,12 +536,15 @@ class PrinterEddyProbe: sensors = { "ldc1612": ldc1612.LDC1612 } sensor_type = config.getchoice('sensor_type', {s: s for s in sensors}) self.sensor_helper = sensors[sensor_type](config, self.calibration) + # Create trigger_analog interface + trig_analog = trigger_analog.MCU_trigger_analog(self.sensor_helper) + probe.LookupZSteppers(config, trig_analog.get_dispatch().add_stepper) # Probe interface self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( config, self.sensor_helper, self.calibration, self.probe_offsets, - self.param_helper) + self.param_helper, trig_analog) self.cmd_helper = probe.ProbeCommandHelper(config, self, replace_z_offset=True) self.probe_session = probe.ProbeSessionHelper( From 08f4b65c7c8597fdd26c33a3488a3c282c50e8d0 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 26 Jan 2026 14:28:17 -0500 Subject: [PATCH 091/108] probe_eddy_current: Make EddyScanningProbe a long lived class Create the class at the start of PrinterEddyProbe and call it as needed. This makes the class life-cycle similar to EddyDescend. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 909a6cbcc..4e23848c3 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -475,17 +475,15 @@ class EddyEndstopWrapper: # Implementing probing with "METHOD=scan" class EddyScanningProbe: - def __init__(self, printer, sensor_helper, calibration, probe_offsets, - gcmd): - calibration.verify_calibrated() - self._printer = printer + def __init__(self, config, sensor_helper, calibration, probe_offsets): + self._printer = config.get_printer() self._sensor_helper = sensor_helper self._calibration = calibration self._offsets = probe_offsets.get_offsets() - self._gather = EddyGatherSamples(printer, sensor_helper) + self._gather = None self._sample_time_delay = 0.050 - self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) - self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + self._sample_time = 0. + self._is_rapid = False def _lookup_toolhead_pos(self, pos_time): toolhead = self._printer.lookup_object('toolhead') kin = toolhead.get_kinematics() @@ -502,6 +500,13 @@ class EddyScanningProbe: end_time = start_time + self._sample_time self._gather.add_probe_request(self._analyze_scan, start_time, end_time, printtime) + # Probe session interface + def start_probe_session(self, gcmd): + self._calibration.verify_calibrated() + self._gather = EddyGatherSamples(self._printer, self._sensor_helper) + self._sample_time = gcmd.get_float("SAMPLE_TIME", 0.100, above=0.0) + self._is_rapid = gcmd.get("METHOD", "scan") == 'rapid_scan' + return self def run_probe(self, gcmd): toolhead = self._printer.lookup_object("toolhead") if self._is_rapid: @@ -539,7 +544,7 @@ class PrinterEddyProbe: # Create trigger_analog interface trig_analog = trigger_analog.MCU_trigger_analog(self.sensor_helper) probe.LookupZSteppers(config, trig_analog.get_dispatch().add_stepper) - # Probe interface + # Basic probe requests self.probe_offsets = probe.ProbeOffsetsHelper(config) self.param_helper = probe.ProbeParameterHelper(config) self.eddy_descend = EddyDescend( @@ -549,9 +554,14 @@ class PrinterEddyProbe: replace_z_offset=True) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self.eddy_descend.start_probe_session) + # Create wrapper to support Z homing with probe mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) probe.HomingViaProbeHelper( config, mcu_probe, self.probe_offsets, self.param_helper) + # Probing via "scan" and "rapid_scan" requests + self.eddy_scan = EddyScanningProbe(config, self.sensor_helper, + self.calibration, self.probe_offsets) + # Register with main probe interface self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) @@ -564,8 +574,7 @@ class PrinterEddyProbe: def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): - return EddyScanningProbe(self.printer, self.sensor_helper, - self.calibration, self.probe_offsets, gcmd) + return self.eddy_scan.start_probe_session(gcmd) return self.probe_session.start_probe_session(gcmd) def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) From 5c23f9296a42bd2ff8404e78f17ecf88d061ef23 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 22 Jan 2026 12:31:44 -0500 Subject: [PATCH 092/108] probe_eddy_current: implement tap support Use SOS filters + derivative filter to generate dF/dT on mcu. Feed that to the MCU's trigger_analog peak detection. Interpret peak time as a tap event Signed-off-by: Kevin O'Connor Signed-off-by: Timofey Titovets --- docs/Config_Reference.md | 3 ++ klippy/extras/probe_eddy_current.py | 83 ++++++++++++++++++++++++++++- klippy/extras/trigger_analog.py | 12 +++++ test/klippy/eddy.cfg | 1 + test/klippy/eddy.test | 4 ++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b01360adf..8e7ed287a 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2293,6 +2293,9 @@ sensor_type: ldc1612 #samples_tolerance: #samples_tolerance_retries: # See the "probe" section for information on these parameters. +#tap_threshold: 0 +# Noise cutoff/stop trigger threshold delta Hz per sample +# See the Eddy_Probe.md for explanation ``` ### [axis_twist_compensation] diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 4e23848c3..509fe923f 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -404,6 +404,9 @@ class EddyDescend: self._z_min_position = probe.lookup_minimum_z(config) self._gather = None def _prep_trigger_analog(self): + sos_filter = self._trigger_analog.get_sos_filter() + sos_filter.set_filter_design(None) + sos_filter.set_offset_scale(0, 1.) self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) z_offset = self._probe_offsets.get_offsets()[2] trigger_freq = self._calibration.height_to_freq(z_offset) @@ -473,6 +476,74 @@ class EddyEndstopWrapper: z_offset = self._eddy_descend._probe_offsets.get_offsets()[2] return z_offset +# Probing helper for "tap" requests +class EddyTap: + def __init__(self, config, sensor_helper, calibration, + param_helper, trigger_analog): + self._printer = config.get_printer() + self._sensor_helper = sensor_helper + self._calibration = calibration + self._param_helper = param_helper + self._trigger_analog = trigger_analog + self._z_min_position = probe.lookup_minimum_z(config) + self._gather = None + self._filter_design = None + self._tap_threshold = config.getfloat('tap_threshold', 0., minval=0.) + if self._tap_threshold: + self._setup_tap() + # Setup for "tap" probe request + def _setup_tap(self): + # Create sos filter "design" + cfg_error = self._printer.config_error + sps = self._sensor_helper.get_samples_per_second() + design = trigger_analog.DigitalFilter(sps, cfg_error, + lowpass=25.0, lowpass_order=4) + # Create the derivative (sample to sample difference) post filter + self._filter_design = trigger_analog.DerivativeFilter(design) + # Create SOS filter + cmd_queue = self._trigger_analog.get_dispatch().get_command_queue() + mcu = self._sensor_helper.get_mcu() + sos_filter = trigger_analog.MCU_SosFilter(mcu, cmd_queue, 5) + self._trigger_analog.setup_sos_filter(sos_filter) + def _prep_trigger_analog_tap(self): + if not self._tap_threshold: + raise self._printer.command_error("Tap not configured") + sos_filter = self._trigger_analog.get_sos_filter() + sos_filter.set_filter_design(self._filter_design) + sos_filter.set_offset_scale(0, 1., auto_offset=True) + self._trigger_analog.set_raw_range(0, MAX_VALID_RAW_VALUE) + convert_frequency = self._sensor_helper.convert_frequency + raw_threshold = convert_frequency(self._tap_threshold) + self._trigger_analog.set_trigger('diff_peak_gt', raw_threshold) + # Measurement analysis to determine "tap" position + def _analyze_tap(self, measures, trig_pos): + # XXX - for now just use trigger position (this is not very accurate) + return manual_probe.ProbeResult(trig_pos[0], trig_pos[1], trig_pos[2], + trig_pos[0], trig_pos[1], trig_pos[2]) + # Probe session interface + def start_probe_session(self, gcmd): + self._prep_trigger_analog_tap() + self._gather = EddyGatherSamples(self._printer, self._sensor_helper) + return self + def run_probe(self, gcmd): + toolhead = self._printer.lookup_object('toolhead') + pos = toolhead.get_position() + pos[2] = self._z_min_position + speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] + # Perform probing move + phoming = self._printer.lookup_object('homing') + trig_pos = phoming.probing_move(self._trigger_analog, pos, speed) + # Extract samples + start_time = self._trigger_analog.get_last_trigger_time() - 0.025 + end_time = start_time + 0.025 + self._gather.add_probe_request(self._analyze_tap, + start_time, end_time, trig_pos) + def pull_probed_results(self): + return self._gather.pull_probed() + def end_probe_session(self): + self._gather.finish() + self._gather = None + # Implementing probing with "METHOD=scan" class EddyScanningProbe: def __init__(self, config, sensor_helper, calibration, probe_offsets): @@ -553,11 +624,14 @@ class PrinterEddyProbe: self.cmd_helper = probe.ProbeCommandHelper(config, self, replace_z_offset=True) self.probe_session = probe.ProbeSessionHelper( - config, self.param_helper, self.eddy_descend.start_probe_session) + config, self.param_helper, self._start_descend_wrapper) # Create wrapper to support Z homing with probe mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) probe.HomingViaProbeHelper( config, mcu_probe, self.probe_offsets, self.param_helper) + # Probing via "tap" interface + self.eddy_tap = EddyTap(config, self.sensor_helper, self.calibration, + self.param_helper, trig_analog) # Probing via "scan" and "rapid_scan" requests self.eddy_scan = EddyScanningProbe(config, self.sensor_helper, self.calibration, self.probe_offsets) @@ -568,9 +642,16 @@ class PrinterEddyProbe: def get_probe_params(self, gcmd=None): return self.param_helper.get_probe_params(gcmd) def get_offsets(self, gcmd=None): + if gcmd is not None and gcmd.get('METHOD', '').lower() == "tap": + return (0., 0., 0.) return self.probe_offsets.get_offsets(gcmd) def get_status(self, eventtime): return self.cmd_helper.get_status(eventtime) + def _start_descend_wrapper(self, gcmd): + method = gcmd.get('METHOD', 'automatic').lower() + if method == "tap": + return self.eddy_tap.start_probe_session(gcmd) + return self.eddy_descend.start_probe_session(gcmd) def start_probe_session(self, gcmd): method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index f053611d8..41bec07f7 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -69,6 +69,18 @@ class DigitalFilter: def get_initial_state(self): return self.initial_state +# Produce sample to sample difference (derivative) of a DigitalFilter +class DerivativeFilter: + def __init__(self, main_filter): + self._main_filter = main_filter + + def get_filter_sections(self): + s = list(self._main_filter.get_filter_sections()) + return s + [(1., -1., 0., 1., 0., 0.)] + + def get_initial_state(self): + s = list(self._main_filter.get_initial_state()) + return s + [(-1., 0.)] # Control an `sos_filter` object on the MCU class MCU_SosFilter: diff --git a/test/klippy/eddy.cfg b/test/klippy/eddy.cfg index 11946ebe4..6f26899ef 100644 --- a/test/klippy/eddy.cfg +++ b/test/klippy/eddy.cfg @@ -63,6 +63,7 @@ y_offset: -4 sensor_type: ldc1612 speed: 10.0 intb_pin: PK7 +tap_threshold: 30 [bed_mesh] mesh_min: 10,10 diff --git a/test/klippy/eddy.test b/test/klippy/eddy.test index 5251be122..beeacf949 100644 --- a/test/klippy/eddy.test +++ b/test/klippy/eddy.test @@ -23,7 +23,11 @@ BED_MESH_CALIBRATE METHOD=rapid_scan # Move again G1 Z5 X0 Y0 +# Do "tap" probe +PROBE METHOD=tap + # Do regular probe +G1 Z5 PROBE # Move again From a03eed71150e72d5abb6b536e3b91d5c941e4d87 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Tue, 9 Dec 2025 00:35:45 +0100 Subject: [PATCH 093/108] docs: describe tap calibration routine Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ docs/G-Codes.md | 14 +++++---- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 11069c4bc..58b3abb7f 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -55,6 +55,78 @@ surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. +## Tap calibration + +The Eddy probe measures the resonance frequency of the coil. +By the absolute value of the frequency and the calibration curve from +`PROBE_EDDY_CURRENT_CALIBRATE`, it is therefore possible to detect +where the bed is without physical contact. + +By use of the same knowledge, we know that frequency changes with +the distance. It is possible to track that change in real time and +detect the time/position where contact happens - a change of frequency +starts to change in a different way. +For example, stopped to change because of the collision. + +Because eddy output is not perfect: there is sensor noise, +mechanical oscillation, thermal expansion and other discrepancies, +it is required to calibrate the stop threshold for your machine. +Practically, it ensures that the Eddy's output data absolute value +change per second (velocity) is high enough - higher than the noise level, +and that upon collision it always decreases by at least this value. + +``` +[probe_eddy_current my_probe] +# eddy probe configuration... +tap_threshold: 0 +``` + +The suggested calibration routine works as follows: +1. Home Z +2. Place the toolhead at the center of the bed. +3. Move the Z axis far away (30 mm, for example). +4. Run `PROBE METHOD=tap` +5. If it stops before colliding, increase the `tap_threshold`. + +Repeat until the nozzle softly touches the bed. +This is easier to do with a clean nozzle and +by visually inspecting the process. + +You can streamline the process by placing the toolhead in the center once. +Then, upon config restart, trick the machine into thinking that Z is homed. +``` +SET_KINEMATIC_POSITION X= Y= Z=0 +G0 Z5 # Optional retract +PROBE METHOD=tap +``` + +Here is an example sequence of threshold values to test: +``` +1 -> 5 -> 10 -> 20 -> 40 -> 80 -> 160 +160 -> 120 -> 100 +``` +Your value will normally be between those. +- Too high a value leaves a less safe margin for early collision - +if something is between the nozzle and the bed, or if the nozzle +is too close to the bed before the tap. +- Too low - can make the toolhead stop in mid-air +because of the noise. + +You can validate the tap precision by measuring the paper thickness +from the initial calibration guide. It is expected to be ~0.1mm. + +Tap precision is limited by the sampling frequency and +the speed of the descent. +If you take 24 photos per second of the moving train, you can only estimate +where the train was between photos. + +It is possible to reduce the descending speed. It may require decrease of +absolute `tap_threshold` value. + +It is possible to tap over non-conductive surfaces as long as there is metal +behind it within the sensor's sensitivity range. +Max distance can be approximated to be about 1.5x of the coil's narrowest part. + ## Thermal Drift Calibration As with all inductive probes, eddy current probes are subject to diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 893993e85..6869cbc62 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -1160,23 +1160,25 @@ The following commands are available when a see the [probe calibrate guide](Probe_Calibrate.md)). #### PROBE -`PROBE [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] -[SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] +`PROBE [METHOD=] [PROBE_SPEED=] [LIFT_SPEED=] +[SAMPLES=] [SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] [SAMPLES_TOLERANCE_RETRIES=] [SAMPLES_RESULT=median|average]`: Move the nozzle downwards until the probe triggers. If any of the optional parameters are provided they override their equivalent setting in the [probe config section](Config_Reference.md#probe). +The optional parameter `METHOD` is probe-specific. #### QUERY_PROBE `QUERY_PROBE`: Report the current status of the probe ("triggered" or "open"). #### PROBE_ACCURACY -`PROBE_ACCURACY [PROBE_SPEED=] [SAMPLES=] +`PROBE_ACCURACY [METHOD=] [PROBE_SPEED=] [SAMPLES=] [SAMPLE_RETRACT_DIST=]`: Calculate the maximum, minimum, average, median, and standard deviation of multiple probe samples. By default, 10 SAMPLES are taken. Otherwise the optional parameters default to their equivalent setting in the probe config section. +The optional parameter `METHOD` is probe-specific. #### PROBE_CALIBRATE `PROBE_CALIBRATE [SPEED=] [=]`: Run a @@ -1237,13 +1239,14 @@ The following commands are available when the is enabled. #### QUAD_GANTRY_LEVEL -`QUAD_GANTRY_LEVEL [RETRIES=] [RETRY_TOLERANCE=] +`QUAD_GANTRY_LEVEL [METHOD=] [RETRIES=] [RETRY_TOLERANCE=] [HORIZONTAL_MOVE_Z=] [=]`: This command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values override those options specified in the config file. +The optional parameter `METHOD` is probe-specific. ### [query_adc] @@ -1672,10 +1675,11 @@ The following commands are available when the [z_tilt config section](Config_Reference.md#z_tilt) is enabled. #### Z_TILT_ADJUST -`Z_TILT_ADJUST [RETRIES=] [RETRY_TOLERANCE=] +`Z_TILT_ADJUST [METHOD=] [RETRIES=] [RETRY_TOLERANCE=] [HORIZONTAL_MOVE_Z=] [=]`: This command will probe the points specified in the config and then make independent adjustments to each Z stepper to compensate for tilt. See the PROBE command for details on the optional probe parameters. The optional `RETRIES`, `RETRY_TOLERANCE`, and `HORIZONTAL_MOVE_Z` values override those options specified in the config file. +The optional parameter `METHOD` is probe-specific. From dfe6d3f066c468da8f5f380a79dcc80924447349 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Wed, 7 Jan 2026 17:16:48 +0100 Subject: [PATCH 094/108] docs: describe calibration output Add a hint about the connection between the calibration output and tap threshold. Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 58b3abb7f..d24aadf31 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -24,6 +24,7 @@ named `[probe_eddy_current my_eddy_probe]` then one would run complete in a few seconds. After it completes, issue a `SAVE_CONFIG` command to save the results to the printer.cfg and restart. +Eddy current is used as a proximity/distance sensor (similar to a laser ruler). The second step in calibration is to correlate the sensor readings to the corresponding Z heights. Home the printer and navigate the toolhead so that the nozzle is near the center of the bed. Then run a @@ -35,7 +36,17 @@ those steps are complete one can `ACCEPT` the position. The tool will then move the toolhead so that the sensor is above the point where the nozzle used to be and run a series of movements to correlate the sensor to Z positions. This will take a couple of minutes. After the -tool completes, issue a `SAVE_CONFIG` command to save the results to +tool completes it will output the sensor performance data: +``` +probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314 in 2525 queries +Total frequency range: 45000.012 Hz +z_offset: 0.250 # noise 0.000200mm, MAD_Hz=11.000 +z_offset: 0.530 # noise 0.000300mm, MAD_Hz=12.000 +z_offset: 1.010 # noise 0.000400mm, MAD_Hz=14.000 +z_offset: 2.010 # noise 0.000600mm, MAD_Hz=12.000 +z_offset: 3.010 # noise 0.000700mm, MAD_Hz=9.000 +``` +issue a `SAVE_CONFIG` command to save the results to the printer.cfg and restart. After initial calibration it is a good idea to verify that the @@ -112,6 +123,19 @@ is too close to the bed before the tap. - Too low - can make the toolhead stop in mid-air because of the noise. +You can estimate the initial threshold value by analyzing your own +calibration routine output: +``` +probe_eddy_current: noise 0.000642mm, MAD_Hz=11.314 +... +z_offset: 1.010 # noise 0.000400mm, MAD_Hz=14.000 +``` +The estimation will be: +``` +MAD_Hz * 2 +11.314 * 2 = 22.628 +``` + You can validate the tap precision by measuring the paper thickness from the initial calibration guide. It is expected to be ~0.1mm. From 5fb9902dda78e0027a3110245e4815094e3fcfe0 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 24 Jan 2026 17:11:41 +0100 Subject: [PATCH 095/108] sos_filter: define filtfilt call To implement host-side analysis of tap data, we need a way to apply the same filtering as on the mcu. As bonus, it cancels the induced signal delay. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 41bec07f7..223f83768 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -38,6 +38,7 @@ class DigitalFilter: return try: import scipy.signal as signal + import numpy except: raise cfg_error("DigitalFilter require the SciPy module") if highpass: @@ -69,6 +70,12 @@ class DigitalFilter: def get_initial_state(self): return self.initial_state + def filtfilt(self, data): + import scipy.signal as signal + import numpy + data = numpy.array(data) + return signal.sosfiltfilt(self.filter_sections, data) + # Produce sample to sample difference (derivative) of a DigitalFilter class DerivativeFilter: def __init__(self, main_filter): From 0795fb014175386ef22bc2d9c42b745e0183b506 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Thu, 20 Nov 2025 00:53:14 +0100 Subject: [PATCH 096/108] probe_eddy_current: analyze tap data To cancel out any lag, filter data on the host Then to avoid derivatives lag, compute central difference. Assume that peak velocity is always the moment right before collision happens. Signed-off-by: Timofey Titovets Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 68 ++++++++++++++++++++++++++--- klippy/extras/trigger_analog.py | 3 ++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 509fe923f..e3dd7270e 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -516,8 +516,60 @@ class EddyTap: raw_threshold = convert_frequency(self._tap_threshold) self._trigger_analog.set_trigger('diff_peak_gt', raw_threshold) # Measurement analysis to determine "tap" position - def _analyze_tap(self, measures, trig_pos): - # XXX - for now just use trigger position (this is not very accurate) + def central_diff(self, times, values): + velocity = [0.0] * len(values) + for i in range(1, len(values) - 1): + delta_v = (values[i+1] - values[i-1]) + delta_t = (times[i+1] - times[i-1]) + velocity[i] = delta_v / delta_t + velocity[0] = (values[1] - values[0]) / (times[1] - times[0]) + velocity[-1] = (values[-1] - values[-2]) / (times[-1] - times[-2]) + return velocity + def validate_samples_time(self, timestamps): + sps = self._sensor_helper.get_samples_per_second() + cycle_time = 1.0 / sps + SYNC_SLACK = 0.001 + for i in range(1, len(timestamps)): + tdiff = timestamps[i] - timestamps[i-1] + if cycle_time + SYNC_SLACK < tdiff: + logging.error("Eddy: Gaps in the data: %.3f < %.3f" % ( + (cycle_time + SYNC_SLACK, tdiff) + )) + break + if cycle_time - SYNC_SLACK > tdiff: + logging.error( + "Eddy: CLKIN frequency too low: %.3f > %.3f" % ( + (cycle_time - SYNC_SLACK, tdiff) + )) + break + def _pull_tap_time(self, measures): + tap_time = [] + tap_value = [] + for time, freq, z in measures: + tap_time.append(time) + tap_value.append(freq) + # If samples have gaps this will not produce adequate data + self.validate_samples_time(tap_time) + # Do the same filtering as on the MCU but without induced lag + main_design = self._filter_design.get_main_filter() + try: + fvals = main_design.filtfilt(tap_value) + except ValueError as e: + raise self._printer.command_error(str(e)) + velocity = self.central_diff(tap_time, fvals) + peak_velocity = max(velocity) + i = velocity.index(peak_velocity) + return tap_time[i] + def _lookup_toolhead_pos(self, pos_time): + toolhead = self._printer.lookup_object('toolhead') + kin = toolhead.get_kinematics() + kin_spos = {s.get_name(): s.mcu_to_commanded_position( + s.get_past_mcu_position(pos_time)) + for s in kin.get_steppers()} + return kin.calc_position(kin_spos) + def _analyze_tap(self, measures): + pos_time = self._pull_tap_time(measures) + trig_pos = self._lookup_toolhead_pos(pos_time) return manual_probe.ProbeResult(trig_pos[0], trig_pos[1], trig_pos[2], trig_pos[0], trig_pos[1], trig_pos[2]) # Probe session interface @@ -530,14 +582,18 @@ class EddyTap: pos = toolhead.get_position() pos[2] = self._z_min_position speed = self._param_helper.get_probe_params(gcmd)['probe_speed'] + move_start_time = toolhead.get_last_move_time() # Perform probing move phoming = self._printer.lookup_object('homing') trig_pos = phoming.probing_move(self._trigger_analog, pos, speed) # Extract samples - start_time = self._trigger_analog.get_last_trigger_time() - 0.025 - end_time = start_time + 0.025 - self._gather.add_probe_request(self._analyze_tap, - start_time, end_time, trig_pos) + trigger_time = self._trigger_analog.get_last_trigger_time() + start_time = trigger_time - 0.250 + if start_time < move_start_time: + # Filter short move + start_time = move_start_time + end_time = trigger_time + self._gather.add_probe_request(self._analyze_tap, start_time, end_time) def pull_probed_results(self): return self._gather.pull_probed() def end_probe_session(self): diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 223f83768..6f79c0486 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -81,6 +81,9 @@ class DerivativeFilter: def __init__(self, main_filter): self._main_filter = main_filter + def get_main_filter(self): + return self._main_filter + def get_filter_sections(self): s = list(self._main_filter.get_filter_sections()) return s + [(1., -1., 0., 1., 0., 0.)] From 1fe9fb3ad414ddcf7ecc1dacd310bf8967b61e2c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 29 Jan 2026 14:53:11 -0500 Subject: [PATCH 097/108] trigger_analog: Don't report trigger time as the peak time There are some rare corner cases where reporting the peak time could cause hard to debug issues (for example, the peak time could theoretically be a significant time prior to the actual trigger time, which could possibly cause unexpected clock rollover issues). Now that the host code does not utilize the peak time for "tap" detection, it can be removed from the mcu code. Signed-off-by: Kevin O'Connor --- klippy/extras/trigger_analog.py | 4 ++-- src/trigger_analog.c | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 6f79c0486..d38bcd777 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -277,7 +277,7 @@ class MCU_trigger_analog: # Lookup commands self._query_state_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c homing=%c trigger_clock=%u", + "trigger_analog_state oid=%c homing=%c homing_clock=%u", oid=self._oid, cq=cmd_queue) self._set_raw_range_cmd = self._mcu.lookup_command( "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i", @@ -334,7 +334,7 @@ class MCU_trigger_analog: def _clear_home(self): self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) params = self._query_state_cmd.send([self._oid]) - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_clock']) + trigger_ticks = self._mcu.clock32_to_clock64(params['homing_clock']) return self._mcu.clock_to_print_time(trigger_ticks) def get_steppers(self): diff --git a/src/trigger_analog.c b/src/trigger_analog.c index a77af68d7..ce9b998bf 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -23,7 +23,6 @@ struct trigger_analog { struct sos_filter *sf; // Trigger value checking int32_t trigger_value, trigger_peak; - uint32_t trigger_clock; uint8_t trigger_type; // Trsync triggering uint8_t flags, trigger_reason, error_reason; @@ -91,14 +90,11 @@ check_trigger(struct trigger_analog *ta, uint32_t time, int32_t value) { switch (ta->trigger_type) { case TT_ABS_GE: - ta->trigger_clock = time; return abs(value) >= ta->trigger_value; case TT_GT: - ta->trigger_clock = time; return value > ta->trigger_value; case TT_DIFF_PEAK_GT: if (value > ta->trigger_peak) { - ta->trigger_clock = time; ta->trigger_peak = value; return 0; } @@ -173,6 +169,7 @@ trigger_analog_update(struct trigger_analog *ta, int32_t sample) if (ret) { trsync_do_trigger(ta->ts, ta->trigger_reason); flags = 0; + ta->homing_clock = time; } ta->flags = flags; @@ -250,8 +247,8 @@ command_trigger_analog_query_state(uint32_t *args) { uint8_t oid = args[0]; struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - sendf("trigger_analog_state oid=%c homing=%c trigger_clock=%u" - , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->trigger_clock); + sendf("trigger_analog_state oid=%c homing=%c homing_clock=%u" + , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->homing_clock); } DECL_COMMAND(command_trigger_analog_query_state , "trigger_analog_query_state oid=%c"); From 576d0ca13de8434d6a52e2a3cd474501f3c3eae6 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 30 Jan 2026 14:00:55 -0500 Subject: [PATCH 098/108] probe_eddy_current: Minor reorg to PrinterEddyProbe startup The EddyTap class doesn't need the calibration object. Add some code comments. Signed-off-by: Kevin O'Connor --- klippy/extras/probe_eddy_current.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index e3dd7270e..1befaf6e9 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -478,11 +478,9 @@ class EddyEndstopWrapper: # Probing helper for "tap" requests class EddyTap: - def __init__(self, config, sensor_helper, calibration, - param_helper, trigger_analog): + def __init__(self, config, sensor_helper, param_helper, trigger_analog): self._printer = config.get_printer() self._sensor_helper = sensor_helper - self._calibration = calibration self._param_helper = param_helper self._trigger_analog = trigger_analog self._z_min_position = probe.lookup_minimum_z(config) @@ -677,21 +675,21 @@ class PrinterEddyProbe: self.eddy_descend = EddyDescend( config, self.sensor_helper, self.calibration, self.probe_offsets, self.param_helper, trig_analog) - self.cmd_helper = probe.ProbeCommandHelper(config, self, - replace_z_offset=True) - self.probe_session = probe.ProbeSessionHelper( - config, self.param_helper, self._start_descend_wrapper) # Create wrapper to support Z homing with probe mcu_probe = EddyEndstopWrapper(self.sensor_helper, self.eddy_descend) - probe.HomingViaProbeHelper( - config, mcu_probe, self.probe_offsets, self.param_helper) + probe.HomingViaProbeHelper(config, mcu_probe, + self.probe_offsets, self.param_helper) # Probing via "tap" interface - self.eddy_tap = EddyTap(config, self.sensor_helper, self.calibration, + self.eddy_tap = EddyTap(config, self.sensor_helper, self.param_helper, trig_analog) # Probing via "scan" and "rapid_scan" requests self.eddy_scan = EddyScanningProbe(config, self.sensor_helper, self.calibration, self.probe_offsets) # Register with main probe interface + self.cmd_helper = probe.ProbeCommandHelper(config, self, + replace_z_offset=True) + self.probe_session = probe.ProbeSessionHelper( + config, self.param_helper, self._start_descend_wrapper) self.printer.add_object('probe', self) def add_client(self, cb): self.sensor_helper.add_client(cb) @@ -712,6 +710,7 @@ class PrinterEddyProbe: method = gcmd.get('METHOD', 'automatic').lower() if method in ('scan', 'rapid_scan'): return self.eddy_scan.start_probe_session(gcmd) + # For "tap" and normal, probe_session can average multiple attempts return self.probe_session.start_probe_session(gcmd) def register_drift_compensation(self, comp): self.calibration.register_drift_compensation(comp) From 85ccd1d9df6ed6a67dc2bddfa52e86445b5d76d2 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 30 Jan 2026 23:10:14 +0100 Subject: [PATCH 099/108] temperature_probe: fix missing kin_pos Signed-off-by: Timofey Titovets --- klippy/extras/temperature_probe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/klippy/extras/temperature_probe.py b/klippy/extras/temperature_probe.py index d42d9e210..f47c4e848 100644 --- a/klippy/extras/temperature_probe.py +++ b/klippy/extras/temperature_probe.py @@ -185,7 +185,7 @@ class TemperatureProbe: def get_temp(self, eventtime=None): return self.last_measurement[0], self.target_temp - def _collect_sample(self, kin_pos, tool_zero_z): + def _collect_sample(self, mpresult, tool_zero_z): probe = self._get_probe() x_offset, y_offset, _ = probe.get_offsets() speeds = self._get_speeds() @@ -198,7 +198,7 @@ class TemperatureProbe: cur_pos[0] -= x_offset cur_pos[1] -= y_offset toolhead.manual_move(cur_pos, move_speed) - return self.cal_helper.collect_sample(kin_pos, tool_zero_z, speeds) + return self.cal_helper.collect_sample(mpresult, tool_zero_z, speeds) def _prepare_next_sample(self, last_temp, tool_zero_z): # Register our own abort command now that the manual @@ -237,7 +237,7 @@ class TemperatureProbe: toolhead = self.printer.lookup_object("toolhead") tool_zero_z = toolhead.get_position()[2] try: - last_temp = self._collect_sample(kin_pos, tool_zero_z) + last_temp = self._collect_sample(mpresult, tool_zero_z) except Exception: self._finalize_drift_cal(False) raise @@ -562,7 +562,7 @@ class EddyDriftCompensation: % (self.name, self.cal_temp) ) - def collect_sample(self, kin_pos, tool_zero_z, speeds): + def collect_sample(self, mpresult, tool_zero_z, speeds): if self.calibration_samples is None: self.calibration_samples = [[] for _ in range(DRIFT_SAMPLE_COUNT)] move_times = [] @@ -616,7 +616,7 @@ class EddyDriftCompensation: zvals = [d[2] for d in data] avg_freq = sum(freqs) / len(freqs) avg_z = sum(zvals) / len(zvals) - kin_z = i * .5 + .05 + kin_pos[2] + kin_z = i * .5 + .05 + mpresult.bed_z logging.info( "Probe Values at Temp %.2fC, Z %.4fmm: Avg Freq = %.6f, " "Avg Measured Z = %.6f" From bc0862ddba4aacd70e5ff83084c68a78f250660c Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 31 Jan 2026 01:10:46 +0100 Subject: [PATCH 100/108] config: cartographer v3 example config Signed-off-by: Timofey Titovets --- config/sample-cartographer-v3.cfg | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 config/sample-cartographer-v3.cfg diff --git a/config/sample-cartographer-v3.cfg b/config/sample-cartographer-v3.cfg new file mode 100644 index 000000000..ee1a6fd41 --- /dev/null +++ b/config/sample-cartographer-v3.cfg @@ -0,0 +1,52 @@ +# This file contains common pin mappings for the Cartographer board V3 +# To use this config, the firmware should be compiled for the +# STM32F042 with "24 MHz crystal" and +# "USB (on PA9/PA10)" or "CAN bus (on PA9/PA10)". +# CAN bus requires PA1 GPIO pin to be set at micro-controller start-up +# The "carto" micro-controller will be used to control +# the components on the board. + +# See docs/Config_Reference.md for a description of parameters. + +[mcu carto] +serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_29000380114330394D363620-if00 +#canbus_uuid: 92cf532ef122 + +#[adxl345 carto] +#cs_pin: carto:PA3 +#spi_bus: spi1_PA6_PA7_PA5 +#axes_map: x,y,z + +[thermistor 50k] +temperature1: 25 +resistance1: 50000 +temperature2: 50 +resistance2: 17940 +temperature3: 100 +resistance3: 3090 + +[temperature_probe carto] +pullup_resistor: 10000 +sensor_type: 50k +sensor_pin: carto:PA4 +min_temp: 0 +max_temp: 125 + +[led carto_led] +white_pin: carto:PB5 +initial_WHITE: 0.03 + +[output_pin _LDC1612_en] +pin: carto:PA15 +value: 0 # enable + +[static_pwm_clock ldc1612_clk_in] +pin: carto:PB4 +frequency: 24000000 + +[probe_eddy_current carto] +sensor_type: ldc1612 +frequency: 24000000 +i2c_address: 42 +i2c_mcu: carto +i2c_bus: i2c1_PB6_PB7 From 7047d80b0f4f6a635161241f25a9917c27636c73 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 31 Jan 2026 21:28:48 +0100 Subject: [PATCH 101/108] docs: eddy probe - tap requires install of scipy Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index d24aadf31..e2790a198 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -89,7 +89,13 @@ and that upon collision it always decreases by at least this value. ``` [probe_eddy_current my_probe] # eddy probe configuration... -tap_threshold: 0 +tap_threshold: 0 # 0 means tap is disabled +``` + +Before setting it to any other value, it is necessary to install `scipy`: + +```bash +~/klippy-env/bin/pip install scipy ``` The suggested calibration routine works as follows: From 4c62220491988bab772142f7932bb414eb36a484 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sat, 31 Jan 2026 21:29:39 +0100 Subject: [PATCH 102/108] docs: eddy probe - tip about PROBE_ACCURACY Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index e2790a198..5d2b395d3 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -142,6 +142,11 @@ MAD_Hz * 2 11.314 * 2 = 22.628 ``` +To further fine tune threshold, one can use `PROBE_ACCURACY METHOD=tap`. +The range is expected to be about 0.02 mm, +with the default probe speed of 5 mm/s. +Elevated coil temperature may increase noise and may require additional tuning. + You can validate the tap precision by measuring the paper thickness from the initial calibration guide. It is expected to be ~0.1mm. From 9a04eb5aabbbc49ed97aaf735c1e96c8bee39d2b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 1 Feb 2026 16:21:08 +0100 Subject: [PATCH 103/108] docs: eddy tap recommended samples/tolerance/retries Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 5d2b395d3..812df992e 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -89,6 +89,10 @@ and that upon collision it always decreases by at least this value. ``` [probe_eddy_current my_probe] # eddy probe configuration... +# Recommended starting values for the tap +#samples: 3 +#samples_tolerance: 0.025 +#samples_tolerance_retries: 3 tap_threshold: 0 # 0 means tap is disabled ``` From 29a494aa9e8f1c1458d587d8331adee6e8d44a5b Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 1 Feb 2026 17:00:07 +0100 Subject: [PATCH 104/108] docs: eddy homing correction macro Signed-off-by: Timofey Titovets --- docs/Eddy_Probe.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/Eddy_Probe.md b/docs/Eddy_Probe.md index 812df992e..8b229545e 100644 --- a/docs/Eddy_Probe.md +++ b/docs/Eddy_Probe.md @@ -4,8 +4,10 @@ This document describes how to use an [eddy current](https://en.wikipedia.org/wiki/Eddy_current) inductive probe in Klipper. -Currently, an eddy current probe can not be used for Z homing. The -sensor can only be used for Z probing. +Currently, an eddy current probe can not precisely home Z (i.e., `G28 Z`). +The sensor can precisely do Z probing (i.e., `PROBE ...`). +Look at the [homing correction](Eddy_Probe.md#homing-correction-macros) +for further details. Start by declaring a [probe_eddy_current config section](Config_Reference.md#probe_eddy_current) @@ -66,6 +68,34 @@ surface temperature or sensor hardware temperature can skew the results. It is important that calibration and probing is only done when the printer is at a stable temperature. +## Homing correction macros + +Because of current limitations, homing and probing +are implemented differently for the eddy sensors. +As a result, homing suffers from an offset error, +while probing handles this correctly. + +To correct the homing offset. +One can use the suggested macro inside the homing override or +inside the starting G-Code. + +[Force move](Config_Reference.md#force_move) section +have to be defined in the config. + +``` +[gcode_macro _RELOAD_Z_OFFSET_FROM_PROBE] +gcode: + {% set Z = printer.toolhead.position.z %} + SET_KINEMATIC_POSITION Z={Z - printer.probe.last_probe_position.z} + +[gcode_macro SET_Z_FROM_PROBE] +gcode: + {% set METHOD = params.METHOD | default("automatic") %} + PROBE METHOD={METHOD} + _RELOAD_Z_OFFSET_FROM_PROBE + G0 Z5 +``` + ## Tap calibration The Eddy probe measures the resonance frequency of the coil. From ca7d90084c9f8bdcdeba93a561c3047cbc08e080 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Sun, 25 Jan 2026 15:05:20 +0100 Subject: [PATCH 105/108] probe: disable PROBE_CALIBRATE for Eddy PROBE_CALIBRATE will try to adjust z_offset Which will produce a confusing outcome and will not do what it is supposed to do Signed-off-by: Timofey Titovets --- klippy/extras/probe.py | 16 ++++++++-------- klippy/extras/probe_eddy_current.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index 73fd8927b..c57f9ef1d 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -38,7 +38,7 @@ def calc_probe_z_average(positions, method='average'): # Helper to implement common probing commands class ProbeCommandHelper: def __init__(self, config, probe, query_endstop=None, - replace_z_offset=False): + can_set_z_offset=True): self.printer = config.get_printer() self.probe = probe self.query_endstop = query_endstop @@ -55,16 +55,16 @@ class ProbeCommandHelper: desc=self.cmd_PROBE_help) # PROBE_CALIBRATE command self.probe_calibrate_info = None - gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, - desc=self.cmd_PROBE_CALIBRATE_help) + if can_set_z_offset: + gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE, + desc=self.cmd_PROBE_CALIBRATE_help) # Other commands gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY, desc=self.cmd_PROBE_ACCURACY_help) - if replace_z_offset: - return - gcode.register_command('Z_OFFSET_APPLY_PROBE', - self.cmd_Z_OFFSET_APPLY_PROBE, - desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) + if can_set_z_offset: + gcode.register_command('Z_OFFSET_APPLY_PROBE', + self.cmd_Z_OFFSET_APPLY_PROBE, + desc=self.cmd_Z_OFFSET_APPLY_PROBE_help) def _move(self, coord, speed): self.printer.lookup_object('toolhead').manual_move(coord, speed) def get_status(self, eventtime): diff --git a/klippy/extras/probe_eddy_current.py b/klippy/extras/probe_eddy_current.py index 1befaf6e9..6952480a3 100644 --- a/klippy/extras/probe_eddy_current.py +++ b/klippy/extras/probe_eddy_current.py @@ -687,7 +687,7 @@ class PrinterEddyProbe: self.calibration, self.probe_offsets) # Register with main probe interface self.cmd_helper = probe.ProbeCommandHelper(config, self, - replace_z_offset=True) + can_set_z_offset=False) self.probe_session = probe.ProbeSessionHelper( config, self.param_helper, self._start_descend_wrapper) self.printer.add_object('probe', self) From 4c89f7f826743b897f4f759a4ca525fcd5bee376 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Wed, 4 Feb 2026 22:54:05 +0100 Subject: [PATCH 106/108] generic_cartesian: Full IQEX printers support (#7165) Signed-off-by: Dmitry Butyugin --- docs/Config_Reference.md | 19 +- docs/G-Codes.md | 5 +- klippy/kinematics/cartesian.py | 4 +- klippy/kinematics/generic_cartesian.py | 169 +++++++---- klippy/kinematics/hybrid_corexy.py | 4 +- klippy/kinematics/hybrid_corexz.py | 4 +- klippy/kinematics/idex_modes.py | 214 +++++++------ test/klippy/corexyuv.cfg | 2 + test/klippy/generic_cartesian_iqex.cfg | 386 ++++++++++++++++++++++++ test/klippy/generic_cartesian_iqex.test | 71 +++++ test/klippy/generic_cartesian_itex.cfg | 320 ++++++++++++++++++++ test/klippy/generic_cartesian_itex.test | 65 ++++ 12 files changed, 1099 insertions(+), 164 deletions(-) create mode 100644 test/klippy/generic_cartesian_iqex.cfg create mode 100644 test/klippy/generic_cartesian_iqex.test create mode 100644 test/klippy/generic_cartesian_itex.cfg create mode 100644 test/klippy/generic_cartesian_itex.test diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 8e7ed287a..8b17d9502 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -743,7 +743,7 @@ max_accel: ``` -Then a user must define three carriages for X, Y, and Z axes, e.g.: +Then a user must define three primary carriages for X, Y, and Z axes, e.g.: ``` [carriage carriage_x] axis: @@ -2455,10 +2455,16 @@ Please note that in this case the `[dual_carriage]` configuration deviates from the configuration described above: ``` [dual_carriage my_dc_carriage] -primary_carriage: -# Defines the matching primary carriage of this dual carriage and -# the corresponding IDEX axis. Must match a name of a defined `[carriage]`. -# This parameter must be provided. +#primary_carriage: +# Defines the matching carriage on the same gantry as this dual carriage and +# the corresponding dual axis. Must match a name of a defined `[carriage]` or +# another independent `[dual_carriage]`. If not set, which is a default, +# defines a dual carriage independent of a `[carriage]` with the same axis +# as this one (e.g. on a different gantry). +#axis: +# Axis of a carriage, either x or y. If 'primary_carriage' is defined, then +# this parameter defaults to the 'axis' parameter of that primary carriage, +# otherwise this parameter must be defined. #safe_distance: # The minimum distance (in mm) to enforce between the dual and the primary # carriages. If a G-Code command is executed that will bring the carriages @@ -2467,7 +2473,8 @@ primary_carriage: # position_min and position_max for the dual and primary carriages. If set # to 0 (or safe_distance is unset and position_min and position_max are # identical for the primary and dual carriages), the carriages proximity -# checks will be disabled. +# checks will be disabled. Only valid for a dual_carriage with a defined +# 'primary_carriage'. endstop_pin: #position_min: position_endstop: diff --git a/docs/G-Codes.md b/docs/G-Codes.md index 6869cbc62..915537411 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -350,8 +350,9 @@ reference a defined primary or dual carriage for `generic_cartesian` kinematics or be 0 (for primary carriage) or 1 (for dual carriage) for all other kinematics supporting IDEX. Setting the mode to `PRIMARY` deactivates the other carriage and makes the specified carriage execute -subsequent G-Code commands as-is. `COPY` and `MIRROR` modes are supported -only for dual carriages. When set to either of these modes, dual carriage +subsequent G-Code commands as-is. Before activating `COPY` or `MIRROR` +mode for a carriage, a different one must be activated as `PRIMARY` on +the same axis. When set to either of these two modes, the carriage will then track the subsequent moves of its primary carriage and either copy relative movements of it (in `COPY` mode) or execute them in the opposite (mirror) direction (in `MIRROR` mode). diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 5362e57d0..67858d0bb 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -32,8 +32,8 @@ class CartKinematics: self.dc_module = idex_modes.DualCarriages( self.printer, [self.rails[self.dual_carriage_axis]], [self.rails[3]], axes=[self.dual_carriage_axis], - safe_dist=dc_config.getfloat( - 'safe_distance', None, minval=0.)) + safe_dist=[dc_config.getfloat( + 'safe_distance', None, minval=0.)]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) # Setup boundary checks diff --git a/klippy/kinematics/generic_cartesian.py b/klippy/kinematics/generic_cartesian.py index 230997be3..afb5d47ac 100644 --- a/klippy/kinematics/generic_cartesian.py +++ b/klippy/kinematics/generic_cartesian.py @@ -4,11 +4,13 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. -import copy, itertools, logging, math +import collections, copy, itertools, logging, math import gcode, mathutil, stepper from . import idex_modes from . import kinematic_stepper as ks +VALID_AXES = ['x', 'y', 'z'] + def mat_mul(a, b): if len(a[0]) != len(b): return None @@ -35,13 +37,11 @@ class MainCarriage: def __init__(self, config): self.rail = stepper.GenericPrinterRail(config) carriage_name = self.rail.get_name(short=True) - valid_axes = ['x', 'y', 'z'] - if carriage_name in valid_axes: - axis_name = config.getchoice('axis', valid_axes, carriage_name) + if carriage_name in VALID_AXES: + self.axis_name = config.getchoice('axis', VALID_AXES, carriage_name) else: - axis_name = config.getchoice('axis', valid_axes) - self.axis = ord(axis_name) - ord('x') - self.axis_name = axis_name + self.axis_name = config.getchoice('axis', VALID_AXES) + self.axis = ord(self.axis_name) - ord('x') self.dual_carriage = None def get_name(self): return self.rail.get_name(short=True) @@ -56,9 +56,7 @@ class MainCarriage: return True return self.dual_carriage.get_dc_module().is_active(self.rail) def set_dual_carriage(self, carriage): - old_dc = self.dual_carriage self.dual_carriage = carriage - return old_dc def get_dual_carriage(self): return self.dual_carriage @@ -78,23 +76,49 @@ class ExtraCarriage: self.endstop_pin, self.name) class DualCarriage: - def __init__(self, config, carriages): + def __init__(self, config): self.printer = config.get_printer() self.rail = stepper.GenericPrinterRail(config) - self.primary_carriage = config.getchoice('primary_carriage', carriages) - if self.primary_carriage.set_dual_carriage(self) is not None: - raise config.error( - "Redefinition of dual_carriage for carriage '%s'" % - self.primary_carriage.get_name()) + self.primary_carriage_name = config.get('primary_carriage', None) + if self.primary_carriage_name is None: + self.axis_name = config.getchoice('axis', VALID_AXES) + self.axis = ord(self.axis_name) - ord('x') + self.safe_dist = None + else: + self.axis_name = config.getchoice('axis', VALID_AXES + [None], None) + self.safe_dist = config.getfloat('safe_distance', None, minval=0.) + self.primary_carriage = self.dual_carriage = None + self.config_error = config.error + def resolve_primary_carriage(self, carriages): + if self.primary_carriage_name is None: + return + if self.primary_carriage_name not in carriages: + raise self.config_error( + "primary_carriage = '%s' for '%s' is not a valid choice" + % (self.primary_carriage_name, self.get_name())) + self.primary_carriage = carriages[self.primary_carriage_name] + axis_name = self.axis_name or self.primary_carriage.axis_name + if axis_name != self.primary_carriage.axis_name: + raise self.config_error("Mismatching axes between carriage '%s' " + "(axis=%s) and dual_carriage '%s' (axis=%s)" + % (self.primary_carriage.get_name(), + self.primary_carriage.axis_name, + self.get_name(), axis_name)) + self.axis = ord(axis_name) - ord('x') + if self.primary_carriage.get_dual_carriage(): + raise self.config_error( + "Multiple dual carriages ('%s', '%s') for carriage '%s'" % + (self.primary_carriage.get_dual_carriage().get_name(), + self.get_name(), self.primary_carriage.get_name())) + self.primary_carriage.set_dual_carriage(self) self.axis = self.primary_carriage.get_axis() if self.axis > 1: - raise config.error("Invalid axis '%s' for dual_carriage" % - "xyz"[self.axis]) - self.safe_dist = config.getfloat('safe_distance', None, minval=0.) + raise self.config_error("Invalid axis '%s' for dual_carriage '%s'" % + ("xyz"[self.axis], self.get_name())) def get_name(self): return self.rail.get_name(short=True) def get_axis(self): - return self.primary_carriage.get_axis() + return self.axis def get_rail(self): return self.rail def get_safe_dist(self): @@ -103,7 +127,13 @@ class DualCarriage: return self.printer.lookup_object('dual_carriage') def is_active(self): return self.get_dc_module().is_active(self.rail) + def set_dual_carriage(self, carriage): + self.dual_carriage = carriage def get_dual_carriage(self): + if self.dual_carriage is not None: + return self.dual_carriage + return self.primary_carriage + def get_primary_carriage(self): return self.primary_carriage def add_stepper(self, kin_stepper): self.rail.add_stepper(kin_stepper.get_stepper()) @@ -116,27 +146,38 @@ class GenericCartesianKinematics: s.set_trapq(toolhead.get_trapq()) self.dc_module = None if self.dc_carriages: - pcs = [dc.get_dual_carriage() for dc in self.dc_carriages] + dc_axes = set(dc.get_axis() for dc in self.dc_carriages) + pcs = ([pc for pc in self.primary_carriages + if pc.get_axis() in dc_axes] + + [dc for dc in self.dc_carriages + if dc.get_primary_carriage() is None]) + dcs = [pc.get_dual_carriage() for pc in pcs] primary_rails = [pc.get_rail() for pc in pcs] - dual_rails = [dc.get_rail() for dc in self.dc_carriages] - axes = [dc.get_axis() for dc in self.dc_carriages] - safe_dist = {dc.get_axis() : dc.get_safe_dist() - for dc in self.dc_carriages} + dual_rails = [dc.get_rail() if dc else None for dc in dcs] + axes = [pc.get_axis() for pc in pcs] + safe_dist = [dc.get_safe_dist() if dc else None for dc in dcs] self.dc_module = dc_module = idex_modes.DualCarriages( self.printer, primary_rails, dual_rails, axes, safe_dist) zero_pos = (0., 0.) - for acs in itertools.product(*zip(pcs, self.dc_carriages)): + for acs in itertools.product(*zip(pcs, dcs)): for c in acs: + if c is None: + continue dc_module.get_dc_rail_wrapper(c.get_rail()).activate( idex_modes.PRIMARY, zero_pos) - dc_rail = c.get_dual_carriage().get_rail() - dc_module.get_dc_rail_wrapper(dc_rail).inactivate(zero_pos) + dc = c.get_dual_carriage() + if dc is not None: + dc_module.get_dc_rail_wrapper(dc.get_rail()).inactivate( + zero_pos) self._check_kinematics(config.error) - for c in pcs: - dc_module.get_dc_rail_wrapper(c.get_rail()).activate( + for dc in self.dc_carriages: + dc_module.get_dc_rail_wrapper(dc.get_rail()).inactivate( + zero_pos) + for pc in self.primary_carriages: + if pc.get_axis() not in dc_axes: + continue + dc_module.get_dc_rail_wrapper(pc.get_rail()).activate( idex_modes.PRIMARY, zero_pos) - dc_rail = c.get_dual_carriage().get_rail() - dc_module.get_dc_rail_wrapper(dc_rail).inactivate(zero_pos) else: self._check_kinematics(config.error) # Setup boundary checks @@ -152,25 +193,32 @@ class GenericCartesianKinematics: self.cmd_SET_STEPPER_CARRIAGES, desc=self.cmd_SET_STEPPER_CARRIAGES_help) def _load_kinematics(self, config): - carriages = {} + primary_carriages = [] for mcconfig in config.get_prefix_sections('carriage '): - c = MainCarriage(mcconfig) - axis = c.get_axis() - dups = [mc for mc in carriages.values() if mc.get_axis() == axis] - if dups: + primary_carriages.append(MainCarriage(mcconfig)) + for axis, axis_name in enumerate(VALID_AXES): + dups = [pc.get_name() for pc in primary_carriages + if pc.get_axis() == axis] + if len(dups) > 1: raise config.error( - "Axis '%s' referenced by multiple carriages (%s, %s)" - % ("xyz"[axis], c.get_name(), dups[0].get_name())) - carriages[c.get_name()] = c + "Axis '%s' is set for multiple primary carriages (%s)" + % (axis_name, ', '.join(dups))) + elif not dups: + raise config.error( + "No carriage defined for axis '%s'" % axis_name) dc_carriages = [] for dcconfig in config.get_prefix_sections('dual_carriage '): - dc_carriages.append(DualCarriage(dcconfig, carriages)) - for dc in dc_carriages: - name = dc.get_name() + dc_carriages.append(DualCarriage(dcconfig)) + carriages = {} + for carriage in primary_carriages + dc_carriages: + name = carriage.get_name() if name in carriages: raise config.error("Redefinition of carriage %s" % name) - carriages[name] = dc + carriages[name] = carriage + for dc in dc_carriages: + dc.resolve_primary_carriage(carriages) self.carriages = dict(carriages) + self.primary_carriages = primary_carriages self.dc_carriages = dc_carriages ec_carriages = [] for ecconfig in config.get_prefix_sections('extra_carriage '): @@ -207,16 +255,19 @@ class GenericCartesianKinematics: def get_steppers(self): return [s.get_stepper() for s in self.kin_steppers] def get_primary_carriages(self): - carriages = [None] * 3 - for carriage in self.carriages.values(): - a = carriage.get_axis() - if carriage.get_dual_carriage() is not None: + carriages = [] + for a in range(3): + c = None + if self.dc_module is not None and a in self.dc_module.get_axes(): primary_rail = self.dc_module.get_primary_rail(a) for c in self.carriages.values(): if c.get_rail() == primary_rail: - carriages[a] = c + break else: - carriages[a] = carriage + for c in self.primary_carriages: + if c.get_axis() == a: + break + carriages.append(c) return carriages def _get_kinematics_coeffs(self): matr = {s.get_name() : list(s.get_kin_coeffs()) @@ -227,9 +278,9 @@ class GenericCartesianKinematics: [0. for s in self.kin_steppers]) axes = [dc.get_axis() for dc in self.dc_carriages] orig_matr = copy.deepcopy(matr) - for dc in self.dc_carriages: - axis = dc.get_axis() - for c in [dc.get_dual_carriage(), dc]: + for c in self.carriages.values(): + axis = c.get_axis() + if axis in self.dc_module.get_axes(): m, o = self.dc_module.get_transform(c.get_rail()) for s in c.get_rail().get_steppers(): matr[s.get_name()][axis] *= m @@ -289,16 +340,14 @@ class GenericCartesianKinematics: homing_state.home_rails([rail], forcepos, homepos) def home(self, homing_state): self._check_kinematics(self.printer.command_error) + primary_carriages = self.get_primary_carriages() # Each axis is homed independently and in order for axis in homing_state.get_axes(): - for carriage in self.carriages.values(): - if carriage.get_axis() != axis: - continue - if carriage.get_dual_carriage() != None: - self.dc_module.home(homing_state, axis) - else: - self.home_axis(homing_state, axis, carriage.get_rail()) - break + if self.dc_module is not None and axis in self.dc_module.get_axes(): + self.dc_module.home(homing_state, axis) + else: + carriage = primary_carriages[axis] + self.home_axis(homing_state, axis, carriage.get_rail()) def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index bbe7bfa55..1cd2aa8bc 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -35,8 +35,8 @@ class HybridCoreXYKinematics: self.rails[3].setup_itersolve('corexy_stepper_alloc', b'+') self.dc_module = idex_modes.DualCarriages( self.printer, [self.rails[0]], [self.rails[3]], axes=[0], - safe_dist=dc_config.getfloat( - 'safe_distance', None, minval=0.)) + safe_dist=[dc_config.getfloat( + 'safe_distance', None, minval=0.)]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) # Setup boundary checks diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 3e2dd4788..03e889376 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -35,8 +35,8 @@ class HybridCoreXZKinematics: self.rails[3].setup_itersolve('corexz_stepper_alloc', b'+') self.dc_module = idex_modes.DualCarriages( self.printer, [self.rails[0]], [self.rails[3]], axes=[0], - safe_dist=dc_config.getfloat( - 'safe_distance', None, minval=0.)) + safe_dist=[dc_config.getfloat( + 'safe_distance', None, minval=0.)]) for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) # Setup boundary checks diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index 46b0be08b..4fe6df830 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -14,36 +14,37 @@ MIRROR = 'MIRROR' class DualCarriages: VALID_MODES = [PRIMARY, COPY, MIRROR] - def __init__(self, printer, primary_rails, dual_rails, axes, - safe_dist={}): + def __init__(self, printer, primary_rails, dual_rails, axes, safe_dist): self.printer = printer - self.axes = axes self._init_steppers(primary_rails + dual_rails) - self.primary_rails = [ - DualCarriagesRail(printer, c, dual_rails[i], - axes[i], active=True) - for i, c in enumerate(primary_rails)] + safe_dist = list(safe_dist) + for i, dc in enumerate(dual_rails): + if dc is None or safe_dist[i] is not None: + continue + pc = primary_rails[i] + safe_dist[i] = min(abs(pc.position_min - dc.position_min), + abs(pc.position_max - dc.position_max)) + self.primary_mode_dcs = [None] * 3 + self.primary_rails = [] + for i, c in enumerate(primary_rails): + activate = self.primary_mode_dcs[axes[i]] is None + dc_rail = DualCarriagesRail( + printer, c, dual_rails[i], axes[i], safe_dist[i], + active=activate) + if activate: + self.primary_mode_dcs[axes[i]] = dc_rail + self.primary_rails.append(dc_rail) self.dual_rails = [ DualCarriagesRail(printer, c, primary_rails[i], - axes[i], active=False) + axes[i], safe_dist[i], active=False) + if c is not None else None for i, c in enumerate(dual_rails)] self.dc_rails = collections.OrderedDict( [(c.rail.get_name(short=True), c) - for c in self.primary_rails + self.dual_rails]) + for c in self.primary_rails + self.dual_rails + if c is not None]) self.saved_states = {} - self.safe_dist = {} - for i, dc in enumerate(dual_rails): - axis = axes[i] - if isinstance(safe_dist, dict): - if axis in safe_dist: - self.safe_dist[axis] = safe_dist[axis] - continue - elif safe_dist is not None: - self.safe_dist[axis] = safe_dist - continue - pc = primary_rails[i] - self.safe_dist[axis] = min(abs(pc.position_min - dc.position_min), - abs(pc.position_max - dc.position_max)) + self.axes = sorted(set(axes)) self.printer.add_object('dual_carriage', self) self.printer.register_event_handler("klippy:ready", self._handle_ready) gcode = self.printer.lookup_object('gcode') @@ -64,6 +65,8 @@ class DualCarriages: self.orig_stepper_kinematics = [] steppers = set() for rail in rails: + if rail is None: + continue c_steppers = rail.get_steppers() if not c_steppers: raise self.printer.config_error( @@ -80,10 +83,9 @@ class DualCarriages: def get_axes(self): return self.axes def get_primary_rail(self, axis): - for dc_rail in self.dc_rails.values(): - if dc_rail.mode == PRIMARY and dc_rail.axis == axis: - return dc_rail.rail - return None + if self.primary_mode_dcs[axis] is None: + return None + return self.primary_mode_dcs[axis].rail def get_dc_rail_wrapper(self, rail): for dc_rail in self.dc_rails.values(): if dc_rail.rail == rail: @@ -109,22 +111,27 @@ class DualCarriages: if target_dc.mode != PRIMARY: newpos = pos[:axis] + [target_dc.get_axis_position(pos)] \ + pos[axis+1:] + self.primary_mode_dcs[axis] = target_dc target_dc.activate(PRIMARY, newpos, old_position=pos) toolhead.set_position(newpos) kin.update_limits(axis, target_dc.rail.get_range()) def home(self, homing_state, axis): kin = self.printer.lookup_object('toolhead').get_kinematics() - dcs = [dc for dc in self.dc_rails.values() if dc.axis == axis] - if (self.get_dc_order(dcs[0], dcs[1]) > 0) != \ - dcs[0].rail.get_homing_info().positive_dir: - # The second carriage must home first, because the carriages home in - # the same direction and the first carriage homes on the second one - dcs.reverse() - for dc in dcs: - self.toggle_active_dc_rail(dc) - kin.home_axis(homing_state, axis, dc.rail) - # Restore the original rails ordering - self.activate_dc_mode(dcs[0], PRIMARY) + homing_rails = [r for r in self.primary_rails if r.axis == axis] + for dc_rail in homing_rails: + dcs = [dc for dc in self.dc_rails.values() + if dc_rail.rail in [dc.rail, dc.dual_rail]] + if len(dcs) > 1 and (self.get_dc_order(dcs[0], dcs[1]) > 0) != \ + dcs[0].rail.get_homing_info().positive_dir: + # The second carriage must home first, because the carriages + # home in the same direction and the first carriage homes on + # the second one, so reversing the oder + dcs.reverse() + for dc in dcs: + self.toggle_active_dc_rail(dc) + kin.home_axis(homing_state, dc.axis, dc.rail) + # Restore the first rail as primary after all homed + self.activate_dc_mode(homing_rails[0], PRIMARY) def get_status(self, eventtime=None): status = {'carriages' : {dc.get_name() : dc.mode for dc in self.dc_rails.values()}} @@ -132,46 +139,62 @@ class DualCarriages: status.update({('carriage_%d' % (i,)) : dc.mode for i, dc in enumerate(self.dc_rails.values())}) return status - def get_kin_range(self, toolhead, mode, axis): + def get_kin_range(self, toolhead, axis): pos = toolhead.get_position() - dcs = [dc for dc in self.dc_rails.values() if dc.axis == axis] - axes_pos = [dc.get_axis_position(pos) for dc in dcs] - dc0_rail = dcs[0].rail - dc1_rail = dcs[1].rail - if mode != PRIMARY or dcs[0].is_active(): - range_min = dc0_rail.position_min - range_max = dc0_rail.position_max - else: - range_min = dc1_rail.position_min - range_max = dc1_rail.position_max - safe_dist = self.safe_dist[axis] - if not safe_dist: - return (range_min, range_max) + primary_carriage = self.primary_mode_dcs[axis] + if primary_carriage is None: + return (1.0, -1.0) + primary_pos = primary_carriage.get_axis_position(pos) + range_min = primary_carriage.rail.position_min + range_max = primary_carriage.rail.position_max + for carriage in self.dc_rails.values(): + if carriage.axis != axis: + continue + dcs = [carriage] + [dc for dc in self.dc_rails.values() + if carriage.rail is dc.dual_rail] + axes_pos = [dc.get_axis_position(pos) for dc in dcs] + # Check how dcs[0] affects the motion range of primary_carriage + if not dcs[0].is_active(): + continue + elif dcs[0].mode == COPY: + range_min = max(range_min, primary_pos + + dcs[0].rail.position_min - axes_pos[0]) + range_max = min(range_max, primary_pos + + dcs[0].rail.position_max - axes_pos[0]) + elif dcs[0].mode == MIRROR: + range_min = max(range_min, primary_pos + + axes_pos[0] - dcs[0].rail.position_max) + range_max = min(range_max, primary_pos + + axes_pos[0] - dcs[0].rail.position_min) + safe_dist = dcs[0].safe_dist + if not safe_dist or len(dcs) == 1: + continue + if dcs[0].mode == dcs[1].mode or \ + set((dcs[0].mode, dcs[1].mode)) == set((PRIMARY, COPY)): + # dcs[0] and dcs[1] carriages move in the same direction and + # cannot collide with each other + continue - if mode == COPY: - range_min = max(range_min, - axes_pos[0] - axes_pos[1] + dc1_rail.position_min) - range_max = min(range_max, - axes_pos[0] - axes_pos[1] + dc1_rail.position_max) - elif mode == MIRROR: + # Compute how much dcs[0] can move towards dcs[1] + dcs_dist = axes_pos[1] - axes_pos[0] if self.get_dc_order(dcs[0], dcs[1]) > 0: - range_min = max(range_min, - 0.5 * (sum(axes_pos) + safe_dist)) - range_max = min(range_max, - sum(axes_pos) - dc1_rail.position_min) + safe_move_dist = dcs_dist + safe_dist else: - range_max = min(range_max, - 0.5 * (sum(axes_pos) - safe_dist)) - range_min = max(range_min, - sum(axes_pos) - dc1_rail.position_max) - else: - # mode == PRIMARY - active_idx = 1 if dcs[1].is_active() else 0 - inactive_idx = 1 - active_idx - if self.get_dc_order(dcs[active_idx], dcs[inactive_idx]) > 0: - range_min = max(range_min, axes_pos[inactive_idx] + safe_dist) - else: - range_max = min(range_max, axes_pos[inactive_idx] - safe_dist) + safe_move_dist = dcs_dist - safe_dist + if dcs[1].is_active(): + safe_move_dist *= 0.5 + + if dcs[0].mode in (PRIMARY, COPY): + if self.get_dc_order(dcs[0], dcs[1]) > 0: + range_min = max(range_min, primary_pos + safe_move_dist) + else: + range_max = min(range_max, primary_pos + safe_move_dist) + else: # dcs[0].mode == MIRROR + if self.get_dc_order(dcs[0], dcs[1]) > 0: + range_max = min(range_max, primary_pos - safe_move_dist) + else: + range_min = max(range_min, primary_pos - safe_move_dist) + if range_min > range_max: # During multi-MCU homing it is possible that the carriage # position will end up below position_min or above position_max @@ -208,12 +231,13 @@ class DualCarriages: axis = dc.axis if mode == INACTIVE: dc.inactivate(toolhead.get_position()) + if self.primary_mode_dcs[axis] is dc: + self.primary_mode_dcs[axis] = None elif mode == PRIMARY: self.toggle_active_dc_rail(dc) else: - self.toggle_active_dc_rail(self.get_dc_rail_wrapper(dc.dual_rail)) dc.activate(mode, toolhead.get_position()) - kin.update_limits(axis, self.get_kin_range(toolhead, mode, axis)) + kin.update_limits(axis, self.get_kin_range(toolhead, axis)) def _handle_ready(self): for dc_rail in self.dc_rails.values(): dc_rail.apply_transform() @@ -241,10 +265,9 @@ class DualCarriages: if mode not in self.VALID_MODES: raise gcmd.error("Invalid mode=%s specified" % (mode,)) if mode in [COPY, MIRROR]: - if dc_rail in self.primary_rails: + if self.primary_mode_dcs[dc_rail.axis] in [None, dc_rail]: raise gcmd.error( - "Mode=%s is not supported for carriage=%s" % ( - mode, dc_rail.get_name())) + "Must activate another carriage as PRIMARY first") curtime = self.printer.get_reactor().monotonic() kin = self.printer.lookup_object('toolhead').get_kinematics() axis = 'xyz'[dc_rail.axis] @@ -291,18 +314,21 @@ class DualCarriages: for i, dc in enumerate(dcs)] for axis in self.axes: dc_ind = [i for i, dc in enumerate(dcs) if dc.axis == axis] - if abs(dl[dc_ind[0]]) >= abs(dl[dc_ind[1]]): - primary_ind, secondary_ind = dc_ind[0], dc_ind[1] - else: - primary_ind, secondary_ind = dc_ind[1], dc_ind[0] + abs_dl = [abs(dl[i]) for i in dc_ind] + primary_ind = dc_ind[abs_dl.index(max(abs_dl))] primary_dc = dcs[primary_ind] self.toggle_active_dc_rail(primary_dc) move_pos[axis] = carriage_positions[primary_dc.get_name()] - dc_mode = INACTIVE if min(abs(dl[primary_ind]), - abs(dl[secondary_ind])) < .000000001 \ - else COPY if dl[primary_ind] * dl[secondary_ind] > 0 \ - else MIRROR - if dc_mode != INACTIVE: + for secondary_ind in dc_ind: + if secondary_ind == primary_ind: + continue + if min(abs(dl[primary_ind]), + abs(dl[secondary_ind])) < .000000001: + continue + if dl[primary_ind] * dl[secondary_ind] > 0: + dc_mode = COPY + else: + dc_mode = MIRROR dcs[secondary_ind].activate(dc_mode, cur_pos[primary_ind]) dcs[secondary_ind].override_axis_scaling( abs(dl[secondary_ind] / dl[primary_ind]), @@ -312,18 +338,26 @@ class DualCarriages: # Make sure the scaling coefficients are restored with the mode for dc in dcs: dc.inactivate(move_pos) + saved_modes = saved_state['carriage_modes'] + saved_primary_dcs = [dc for dc in self.dc_rails.values() + if saved_modes[dc.get_name()] == PRIMARY] + # First activate all primary carriages + for dc in saved_primary_dcs: + self.activate_dc_mode(dc, PRIMARY) + # Then set the modes the remaining carriages for dc in self.dc_rails.values(): - saved_mode = saved_state['carriage_modes'][dc.get_name()] - self.activate_dc_mode(dc, saved_mode) + if dc not in saved_primary_dcs: + self.activate_dc_mode(dc, saved_modes[dc.get_name()]) class DualCarriagesRail: ENC_AXES = [b'x', b'y'] - def __init__(self, printer, rail, dual_rail, axis, active): + def __init__(self, printer, rail, dual_rail, axis, safe_dist, active): self.printer = printer self.rail = rail self.dual_rail = dual_rail self.sks = [s.get_stepper_kinematics() for s in rail.get_steppers()] self.axis = axis + self.safe_dist = safe_dist self.mode = (INACTIVE, PRIMARY)[active] self.offset = 0. self.scale = 1. if active else 0. diff --git a/test/klippy/corexyuv.cfg b/test/klippy/corexyuv.cfg index e9b0cd02a..e6fd37006 100644 --- a/test/klippy/corexyuv.cfg +++ b/test/klippy/corexyuv.cfg @@ -24,6 +24,7 @@ primary_carriage: carriage_z endstop_pin: ^PD2 [dual_carriage carriage_u] +axis: x primary_carriage: carriage_x safe_distance: 70 position_endstop: 300 @@ -32,6 +33,7 @@ homing_speed: 50 endstop_pin: ^PE4 [dual_carriage carriage_v] +axis: y primary_carriage: carriage_y safe_distance: 50 position_endstop: 200 diff --git a/test/klippy/generic_cartesian_iqex.cfg b/test/klippy/generic_cartesian_iqex.cfg new file mode 100644 index 000000000..aa4c2f5cf --- /dev/null +++ b/test/klippy/generic_cartesian_iqex.cfg @@ -0,0 +1,386 @@ +# Test config for generic cartesian kinematics with quad independent extruders +[mcu] +serial: /dev/ttyACM0 + +[mcu extboard] +serial: /dev/ttyACM1 + +[carriage carriage_t0] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG6 + +[dual_carriage carriage_t1] +primary_carriage: carriage_t0 +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG9 + +[dual_carriage carriage_t2] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG10 + +[dual_carriage carriage_t3] +primary_carriage: carriage_t2 +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG11 + +[carriage carriage_gantry0_left] +axis: y +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: PG6 + +[extra_carriage carriage_gantry0_right] +primary_carriage: carriage_gantry0_left +endstop_pin: PG9 + +[dual_carriage carriage_gantry1_left] +primary_carriage: carriage_gantry0_left +safe_distance: 50 +position_endstop: 200 +position_max: 200 +homing_speed: 50 +endstop_pin: PG10 + +[extra_carriage carriage_gantry1_right] +primary_carriage: carriage_gantry1_left +endstop_pin: PG11 + +[carriage carriage_z0] +axis: z +position_endstop: 0.5 +position_max: 100 +endstop_pin: PG12 + +[extra_carriage carriage_z1] +primary_carriage: carriage_z0 +endstop_pin: PG13 + +[extra_carriage carriage_z2] +primary_carriage: carriage_z0 +endstop_pin: PG14 + +[stepper stepper_t0_x] +carriages: carriage_t0 +step_pin: extboard:PF13 +dir_pin: extboard:PF12 +enable_pin: !extboard:PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t1_x] +carriages: carriage_t1 +step_pin: extboard:PG0 +dir_pin: extboard:PG1 +enable_pin: !extboard:PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t2_x] +carriages: carriage_t2 +step_pin: extboard:PF11 +dir_pin: extboard:PG3 +enable_pin: !extboard:PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t3_x] +carriages: carriage_t3 +step_pin: extboard:PG4 +dir_pin: extboard:PC1 +enable_pin: !extboard:PA2 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_left] +carriages: carriage_gantry0_left +step_pin: PF13 +dir_pin: PF12 +enable_pin: !PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_right] +carriages: carriage_gantry0_right +step_pin: PG0 +dir_pin: PG1 +enable_pin: !PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_left] +carriages: carriage_gantry1_left +step_pin: PF11 +dir_pin: PG3 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_right] +carriages: carriage_gantry1_right +step_pin: PG4 +dir_pin: PC1 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 40 + +[stepper z0] +carriages: carriage_z0 +step_pin: PF9 +dir_pin: PF10 +enable_pin: !PG2 +microsteps: 16 +rotation_distance: 8 + +[stepper z1] +carriages: carriage_z1 +step_pin: PC13 +dir_pin: PF0 +enable_pin: !PF1 +microsteps: 16 +rotation_distance: 8 + +[stepper z2] +carriages: carriage_z2 +step_pin: PE2 +dir_pin: PE3 +enable_pin: !PD4 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: extboard:PF9 +dir_pin: extboard:PF10 +enable_pin: !extboard:PG2 +heater_pin: extboard:PA0 # HE0 +sensor_pin: extboard:PF4 # T0 +microsteps: 16 +rotation_distance: 33.500 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder1] +step_pin: extboard:PC13 +dir_pin: extboard:PF0 +enable_pin: !extboard:PF1 +heater_pin: extboard:PA3 # HE1 +sensor_pin: extboard:PF5 # T1 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder2] +step_pin: extboard:PE2 +dir_pin: extboard:PE3 +enable_pin: !extboard:PD4 +heater_pin: extboard:PB0 # HE2 +sensor_pin: extboard:PF6 # T2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder3] +step_pin: extboard:PE6 +dir_pin: extboard:PA14 +enable_pin: !extboard:PE0 +heater_pin: extboard:PB11 # HE3 +sensor_pin: extboard:PF7 # T3 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_EXTRUDERS] +gcode: + G90 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{printer.configfile.settings["dual_carriage carriage_t3"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + +[gcode_macro T0] +gcode: + PARK_EXTRUDERS + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + +[gcode_macro T1] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1 + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + +[gcode_macro T2] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder2 + ACTIVATE_EXTRUDER EXTRUDER=extruder2 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + +[gcode_macro T3] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder3 + ACTIVATE_EXTRUDER EXTRUDER=extruder3 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + +[gcode_macro SET_COPY_MODE] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + {% set x_max = [printer.configfile.settings["dual_carriage carriage_t3"].position_max, printer.configfile.settings["dual_carriage carriage_t1"].position_max]|min %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE1] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{printer.configfile.settings["dual_carriage carriage_t3"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE2] +gcode: + G90 + {% set x_max = [printer.configfile.settings["dual_carriage carriage_t3"].position_max, printer.configfile.settings["dual_carriage carriage_t1"].position_max]|min %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left + G1 Y{printer.configfile.settings["dual_carriage carriage_gantry1_left"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left MODE=MIRROR + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder3 MOTION_QUEUE=extruder + +[heater_bed] +heater_pin: PA1 +sensor_pin: PF3 # TB +sensor_type: ATC Semitec 104GT-2 +control: watermark +min_temp: 0 +max_temp: 130 + +[fan] +pin: PA8 + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +[input_shaper] diff --git a/test/klippy/generic_cartesian_iqex.test b/test/klippy/generic_cartesian_iqex.test new file mode 100644 index 000000000..b317dbf21 --- /dev/null +++ b/test/klippy/generic_cartesian_iqex.test @@ -0,0 +1,71 @@ +# Test cases on printers with quad independent extruders +DICTIONARY stm32h723.dict extboard=stm32h723.dict +CONFIG generic_cartesian_iqex.cfg + +# First home the printer +G90 +M83 +G28 + +# Perform a dummy move +G1 X10 Y20 Z10 F6000 +G1 X11 E0.1 F3000 + +# Test other tools +T1 +G1 X120 Y50 F6000 +G1 X119 E0.1 F3000 + +T2 +G1 X200 Y70 F6000 +G1 X199 E0.1 F3000 + +T3 +G1 X70 Y50 F6000 +G1 X71 E0.1 F3000 + +# Go back to main tool +T0 +G1 X20 Y100 F6000 + +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 Y100 F6000 + +T2 +# Activate the dual carriage on Y axis +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left +G1 X10 Y150 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + +QUERY_ENDSTOPS + +# Configure input shaper +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1_left +SET_DUAL_CARRIAGE CARRIAGE=carriage_t3 +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=45 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left +SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 +SET_INPUT_SHAPER SHAPER_TYPE_X=mzv SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 +SET_INPUT_SHAPER SHAPER_TYPE_X=zvd SHAPER_FREQ_X=55 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 + +T0 +G1 X100 Y150 F6000 + +SET_COPY_MODE +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE1 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE2 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 diff --git a/test/klippy/generic_cartesian_itex.cfg b/test/klippy/generic_cartesian_itex.cfg new file mode 100644 index 000000000..dc0ae115a --- /dev/null +++ b/test/klippy/generic_cartesian_itex.cfg @@ -0,0 +1,320 @@ +# Test config for generic cartesian kinematics with triple independent extruders +[mcu] +serial: /dev/ttyACM0 + +[mcu extboard] +serial: /dev/ttyACM1 + +[carriage carriage_t0] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG6 + +[dual_carriage carriage_t1] +primary_carriage: carriage_t0 +safe_distance: 70 +position_endstop: 300 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG9 + +[dual_carriage carriage_t2] +axis: x +position_endstop: 0 +position_max: 300 +homing_speed: 50 +endstop_pin: extboard:PG10 + +[carriage carriage_gantry0_left] +axis: y +position_endstop: 0 +position_max: 200 +homing_speed: 50 +endstop_pin: PG6 + +[extra_carriage carriage_gantry0_right] +primary_carriage: carriage_gantry0_left +endstop_pin: PG9 + +[dual_carriage carriage_gantry1] +primary_carriage: carriage_gantry0_left +safe_distance: 50 +position_endstop: 200 +position_max: 200 +homing_speed: 50 +endstop_pin: PG10 + +[carriage carriage_z0] +axis: z +position_endstop: 0.5 +position_max: 100 +endstop_pin: PG12 + +[extra_carriage carriage_z1] +primary_carriage: carriage_z0 +endstop_pin: PG13 + +[extra_carriage carriage_z2] +primary_carriage: carriage_z0 +endstop_pin: PG14 + +[stepper stepper_t0_x] +carriages: carriage_t0 +step_pin: extboard:PF13 +dir_pin: extboard:PF12 +enable_pin: !extboard:PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper stepper_t1_x] +carriages: carriage_t1 +step_pin: extboard:PG0 +dir_pin: extboard:PG1 +enable_pin: !extboard:PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_left] +carriages: carriage_gantry0_left +step_pin: PF13 +dir_pin: PF12 +enable_pin: !PF14 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry0_right] +carriages: carriage_gantry0_right +step_pin: PG0 +dir_pin: PG1 +enable_pin: !PF15 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_a] +carriages: carriage_t2-carriage_gantry1 +step_pin: PF11 +dir_pin: PG3 +enable_pin: !PG5 +microsteps: 16 +rotation_distance: 40 + +[stepper gantry1_b] +carriages: carriage_t2+carriage_gantry1 +step_pin: PG4 +dir_pin: PC1 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 40 + +[stepper z0] +carriages: carriage_z0 +step_pin: PF9 +dir_pin: PF10 +enable_pin: !PG2 +microsteps: 16 +rotation_distance: 8 + +[stepper z1] +carriages: carriage_z1 +step_pin: PC13 +dir_pin: PF0 +enable_pin: !PF1 +microsteps: 16 +rotation_distance: 8 + +[stepper z2] +carriages: carriage_z2 +step_pin: PE2 +dir_pin: PE3 +enable_pin: !PD4 +microsteps: 16 +rotation_distance: 8 + +[extruder] +step_pin: extboard:PF9 +dir_pin: extboard:PF10 +enable_pin: !extboard:PG2 +heater_pin: extboard:PA0 # HE0 +sensor_pin: extboard:PF4 # T0 +microsteps: 16 +rotation_distance: 33.500 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder1] +step_pin: extboard:PC13 +dir_pin: extboard:PF0 +enable_pin: !extboard:PF1 +heater_pin: extboard:PA3 # HE1 +sensor_pin: extboard:PF5 # T1 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[extruder2] +step_pin: extboard:PE2 +dir_pin: extboard:PE3 +enable_pin: !extboard:PD4 +heater_pin: extboard:PB0 # HE2 +sensor_pin: extboard:PF6 # T2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +sensor_type: EPCOS 100K B57560G104F +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 250 + +[gcode_macro PARK_EXTRUDERS] +gcode: + G90 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{printer.configfile.settings["dual_carriage carriage_gantry1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + +[gcode_macro T0] +gcode: + PARK_EXTRUDERS + ACTIVATE_EXTRUDER EXTRUDER=extruder + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + +[gcode_macro T1] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder1 + ACTIVATE_EXTRUDER EXTRUDER=extruder1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + +[gcode_macro T2] +gcode: + PARK_EXTRUDERS + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder2 + ACTIVATE_EXTRUDER EXTRUDER=extruder2 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + +[gcode_macro SET_COPY_MODE] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + {% set x_max = printer.configfile.settings["dual_carriage carriage_t1"].position_max %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE1] +gcode: + G90 + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + {% set x_max = printer.configfile.settings["dual_carriage carriage_t1"].position_max %} + {% set x_min = [printer.configfile.settings["dual_carriage carriage_t2"].position_min, printer.configfile.settings["carriage carriage_t0"].position_min]|max %} + {% set x_center = 0.5 * (x_max + x_min) %} + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{x_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=COPY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + +[gcode_macro SET_MIRROR_MODE2] +gcode: + {% set y_center = 0.5 * (printer.configfile.settings["dual_carriage carriage_gantry1"].position_max + printer.configfile.settings["carriage carriage_gantry0_left"].position_min) %} + G90 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left + G1 Y{printer.configfile.settings["carriage carriage_gantry0_left"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 + G1 Y{y_center} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 + G1 X{printer.configfile.settings["dual_carriage carriage_t2"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 + G1 X{printer.configfile.settings["carriage carriage_t0"].position_min} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 + G1 X{printer.configfile.settings["dual_carriage carriage_t1"].position_max} F12000 + SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 MODE=MIRROR + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left MODE=PRIMARY + SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 MODE=COPY + ACTIVATE_EXTRUDER EXTRUDER=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder2 MOTION_QUEUE=extruder + +[heater_bed] +heater_pin: PA1 +sensor_pin: PF3 # TB +sensor_type: ATC Semitec 104GT-2 +control: watermark +min_temp: 0 +max_temp: 130 + +[fan] +pin: PA8 + +[printer] +kinematics: generic_cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + +[input_shaper] diff --git a/test/klippy/generic_cartesian_itex.test b/test/klippy/generic_cartesian_itex.test new file mode 100644 index 000000000..bcf80771d --- /dev/null +++ b/test/klippy/generic_cartesian_itex.test @@ -0,0 +1,65 @@ +# Test cases on printers with triple independent extruders +DICTIONARY stm32h723.dict extboard=stm32h723.dict +CONFIG generic_cartesian_itex.cfg + +# First home the printer +G90 +M83 +G28 + +# Perform a dummy move +G1 X10 Y20 Z10 F6000 +G1 X11 E0.1 F3000 + +# Test other tools +T1 +G1 X120 Y50 F6000 +G1 X119 E0.1 F3000 + +T2 +G1 X200 Y70 F6000 +G1 X199 E0.1 F3000 + +# Go back to main tool +T0 +G1 X20 Y100 F6000 + +# Save dual carriage state +SAVE_DUAL_CARRIAGE_STATE + +G1 Y100 F6000 + +T2 +# Activate the dual carriage on Y axis +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 +G1 X10 Y150 F6000 + +# Restore dual carriage state +RESTORE_DUAL_CARRIAGE_STATE + +QUERY_ENDSTOPS + +# Configure input shaper +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry1 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t2 +SET_INPUT_SHAPER SHAPER_TYPE_X=ei SHAPER_FREQ_X=45 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=80 +SET_DUAL_CARRIAGE CARRIAGE=carriage_gantry0_left +SET_DUAL_CARRIAGE CARRIAGE=carriage_t1 +SET_INPUT_SHAPER SHAPER_TYPE_X=mzv SHAPER_FREQ_X=50 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 +SET_DUAL_CARRIAGE CARRIAGE=carriage_t0 +SET_INPUT_SHAPER SHAPER_TYPE_X=zvd SHAPER_FREQ_X=55 SHAPER_TYPE_Y=2hump_ei SHAPER_FREQ_Y=70 + +T0 +G1 X100 Y150 F6000 + +SET_COPY_MODE +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE1 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 + +SET_MIRROR_MODE2 +G1 X10 Y10 F6000 +G1 X11 E0.1 F3000 From 36d9a7758084f15e539995f00e48d60c2c14b6c5 Mon Sep 17 00:00:00 2001 From: bigtreetech Date: Wed, 31 Dec 2025 14:19:08 +0800 Subject: [PATCH 107/108] temperature_combined: add additional sensor values(humidity, pressure, gas) Signed-off-by: Alan.Ma from BigTreeTech tech@biqu3d.com --- klippy/extras/temperature_combined.py | 45 +++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/klippy/extras/temperature_combined.py b/klippy/extras/temperature_combined.py index d032bce03..22b5f4c88 100644 --- a/klippy/extras/temperature_combined.py +++ b/klippy/extras/temperature_combined.py @@ -26,6 +26,7 @@ class PrinterSensorCombined: self.apply_mode = config.getchoice('combination_method', algos) # set default values self.last_temp = self.min_temp = self.max_temp = 0.0 + self.humidity = self.pressure = self.gas = None # add object self.printer.add_object("temperature_combined " + self.name, self) # time-controlled sensor update @@ -96,13 +97,53 @@ class PrinterSensorCombined: def get_temp(self, eventtime): return self.last_temp, 0. + def update_additional(self, eventtime): + values_humidity = [] + values_pressure = [] + values_gas = [] + for sensor in self.sensors: + sensor_status = sensor.get_status(eventtime) + if 'humidity' in sensor_status: + sensor_humidity = sensor_status['humidity'] + if sensor_humidity is not None: + values_humidity.append(sensor_humidity) + if 'pressure' in sensor_status: + sensor_pressure = sensor_status['pressure'] + if sensor_pressure is not None: + values_pressure.append(sensor_pressure) + if 'gas' in sensor_status: + sensor_gas = sensor_status['gas'] + if sensor_gas is not None: + values_gas.append(sensor_gas) + + humidity = self.apply_mode(values_humidity) + if humidity: + self.humidity = humidity + + pressure = self.apply_mode(values_pressure) + if pressure: + self.pressure = pressure + + gas = self.apply_mode(values_gas) + if gas: + self.gas = gas + def get_status(self, eventtime): - return {'temperature': round(self.last_temp, 2), - } + data = { + 'temperature': round(self.last_temp, 2), + } + if self.humidity is not None: + data['humidity'] = self.humidity + if self.pressure is not None: + data['pressure'] = self.pressure + if self.gas is not None: + data['gas'] = self.gas + return data def _temperature_update_event(self, eventtime): # update sensor value self.update_temp(eventtime) + self.update_additional(eventtime) # check min / max temp values if self.last_temp < self.min_temp: From 87ea2ff1cea4f1c1a05217e96a04dbe415f0b002 Mon Sep 17 00:00:00 2001 From: Timofey Titovets Date: Fri, 6 Feb 2026 02:53:47 +0100 Subject: [PATCH 108/108] temperature_combined: fix operations on empty list Signed-off-by: Timofey Titovets --- klippy/extras/temperature_combined.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/klippy/extras/temperature_combined.py b/klippy/extras/temperature_combined.py index 22b5f4c88..51468bc9b 100644 --- a/klippy/extras/temperature_combined.py +++ b/klippy/extras/temperature_combined.py @@ -116,17 +116,20 @@ class PrinterSensorCombined: if sensor_gas is not None: values_gas.append(sensor_gas) - humidity = self.apply_mode(values_humidity) - if humidity: - self.humidity = humidity + if values_humidity: + humidity = self.apply_mode(values_humidity) + if humidity: + self.humidity = humidity - pressure = self.apply_mode(values_pressure) - if pressure: - self.pressure = pressure + if values_pressure: + pressure = self.apply_mode(values_pressure) + if pressure: + self.pressure = pressure - gas = self.apply_mode(values_gas) - if gas: - self.gas = gas + if values_gas: + gas = self.apply_mode(values_gas) + if gas: + self.gas = gas def get_status(self, eventtime): data = {