diff --git a/README.MD b/README.MD index fed2054..5487e2c 100644 --- a/README.MD +++ b/README.MD @@ -51,6 +51,7 @@ Gopper brings the power of Go to 3D printer firmware, providing a modern, type-s - ✅ Pin enumeration system - ✅ GPIO driver infrastructure (RP2040) - ✅ Digital output support + - ✅ Digital input support (pull-up/pull-down) - ✅ Pin enumeration (gpio0-gpio29) - ✅ PWM capable outputs - ✅ PWM driver infrastructure (RP2040) diff --git a/core/adc.go b/core/adc.go index ec642b2..fc65cdc 100644 --- a/core/adc.go +++ b/core/adc.go @@ -51,6 +51,12 @@ type AnalogIn struct { // Global registry of analog inputs var analogInputs = make(map[uint8]*AnalogIn) +// GetADC retrieves an analog input by OID +func GetADC(oid uint8) (*AnalogIn, bool) { + adc, exists := analogInputs[oid] + return adc, exists +} + // Wake flag for analog-in task var analogInWake bool diff --git a/core/command.go b/core/command.go index 68efbc5..6b967f9 100644 --- a/core/command.go +++ b/core/command.go @@ -152,3 +152,9 @@ func DispatchCommand(cmdID uint16, data *[]byte) error { func GetGlobalRegistry() *CommandRegistry { return globalRegistry } + +// RegisterResponse registers a response message (MCU -> Host) +// This is a convenience wrapper around RegisterCommand with a nil handler +func RegisterResponse(name string, format string) uint16 { + return globalRegistry.Register(name, format, nil) +} diff --git a/core/endstop.go b/core/endstop.go new file mode 100644 index 0000000..52b893f --- /dev/null +++ b/core/endstop.go @@ -0,0 +1,305 @@ +// Endstop handling for GPIO-based sensors +// Implements Klipper's endstop protocol for mechanical switches, hall effect sensors, etc. +package core + +import ( + "gopper/protocol" +) + +// Endstop flags +const ( + ESF_PIN_HIGH = 1 << 0 // Expected pin state when triggered (1=high, 0=low) + ESF_HOMING = 1 << 1 // Currently homing +) + +// Endstop represents a configured GPIO endstop +type Endstop struct { + OID uint8 // Object ID + Pin GPIOPin // GPIO pin for endstop input + Flags uint8 // State flags (ESF_*) + Timer Timer // Timer for sampling + SampleTime uint32 // Time between samples (in ticks) + SampleCount uint8 // Number of consecutive samples required + TriggerCount uint8 // Remaining samples before trigger + RestTime uint32 // Rest time between check cycles + NextWake uint32 // Next scheduled wake time + TriggerSync *TriggerSync // Associated trigger synchronization object + TriggerReason uint8 // Reason code to report when triggered +} + +// Global registry of endstops +var endstops = make(map[uint8]*Endstop) + +// InitEndstopCommands registers endstop-related commands +func InitEndstopCommands() { + // Command to configure an endstop + RegisterCommand("config_endstop", "oid=%c pin=%u pull_up=%c", handleConfigEndstop) + + // Command to start homing with an endstop + RegisterCommand("endstop_home", "oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u pin_value=%c trsync_oid=%c trigger_reason=%c", handleEndstopHome) + + // Command to query endstop state + RegisterCommand("endstop_query_state", "oid=%c", handleEndstopQueryState) + + // Response: endstop state report + RegisterResponse("endstop_state", "oid=%c homing=%c next_clock=%u pin_value=%c") +} + +// handleConfigEndstop configures a GPIO endstop +// Format: config_endstop oid=%c pin=%u pull_up=%c +func handleConfigEndstop(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + pin, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + pullUp, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Create new endstop instance + es := &Endstop{ + OID: uint8(oid), + Pin: GPIOPin(pin), + } + + // Configure GPIO pin as input via HAL + // Pull-up/pull-down configuration + if pullUp != 0 { + if err := MustGPIO().ConfigureInputPullUp(es.Pin); err != nil { + return err + } + } else { + if err := MustGPIO().ConfigureInputPullDown(es.Pin); err != nil { + return err + } + } + + // Register in global map + endstops[uint8(oid)] = es + + return nil +} + +// handleEndstopHome starts homing with an endstop +// Format: endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u pin_value=%c trsync_oid=%c trigger_reason=%c +func handleEndstopHome(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + clock, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sampleTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sampleCount, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + restTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + pinValue, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + trsyncOID, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + triggerReason, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get endstop object + es, exists := endstops[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Cancel any existing timer + // Note: In a real implementation, we'd need sched_del_timer + // For now, we clear the Next pointer + es.Timer.Next = nil + + // If sample_count is 0, disable homing + if sampleCount == 0 { + es.TriggerSync = nil + es.Flags = 0 + return nil + } + + // Get trigger sync object + ts, exists := GetTriggerSync(uint8(trsyncOID)) + if !exists { + return nil // Silently ignore if trsync not configured + } + + // Configure homing parameters + es.SampleTime = sampleTicks + es.SampleCount = uint8(sampleCount) + es.TriggerCount = uint8(sampleCount) + es.RestTime = restTicks + es.TriggerSync = ts + es.TriggerReason = uint8(triggerReason) + es.Flags = ESF_HOMING + + // Set expected pin value flag + if pinValue != 0 { + es.Flags |= ESF_PIN_HIGH + } + + // Schedule initial timer + es.Timer.WakeTime = clock + es.Timer.Handler = endstopEvent + ScheduleTimer(&es.Timer) + + return nil +} + +// handleEndstopQueryState queries the current endstop state +// Format: endstop_query_state oid=%c +func handleEndstopQueryState(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get endstop object + es, exists := endstops[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Read current pin state + state := disableInterrupts() + eflags := es.Flags + nextwake := es.NextWake + restoreInterrupts(state) + + // Read pin value + pinValue := uint32(0) + if MustGPIO().ReadPin(es.Pin) { + pinValue = 1 + } + + // Send response + homing := uint32(0) + if (eflags & ESF_HOMING) != 0 { + homing = 1 + } + + SendResponse("endstop_state", func(output protocol.OutputBuffer) { + protocol.EncodeVLQUint(output, uint32(oid)) + protocol.EncodeVLQUint(output, homing) + protocol.EncodeVLQUint(output, nextwake) + protocol.EncodeVLQUint(output, pinValue) + }) + + return nil +} + +// endstopEvent is the timer callback for endstop checking +// This is the first-stage check that looks for a potential trigger +func endstopEvent(t *Timer) uint8 { + // Find the Endstop instance that owns this timer + var es *Endstop + for _, esPtr := range endstops { + if esPtr != nil && &esPtr.Timer == t { + es = esPtr + break + } + } + + if es == nil { + return SF_DONE + } + + // Read pin state + pinHigh := MustGPIO().ReadPin(es.Pin) + + // Check if pin matches expected trigger state + // If ESF_PIN_HIGH is set, we expect high (true) + // If ESF_PIN_HIGH is clear, we expect low (false) + expectHigh := (es.Flags & ESF_PIN_HIGH) != 0 + triggered := (pinHigh && expectHigh) || (!pinHigh && !expectHigh) + + nextWake := t.WakeTime + es.RestTime + + if !triggered { + // No match - reschedule for the next attempt + t.WakeTime = nextWake + return SF_RESCHEDULE + } + + // Potential trigger detected - start oversampling + es.NextWake = nextWake + t.Handler = endstopOversampleEvent + return endstopOversampleEvent(t) +} + +// endstopOversampleEvent is the timer callback for oversampling +// This confirms the trigger by taking multiple consecutive samples +func endstopOversampleEvent(t *Timer) uint8 { + // Find the Endstop instance that owns this timer + var es *Endstop + for _, esPtr := range endstops { + if esPtr != nil && &esPtr.Timer == t { + es = esPtr + break + } + } + + if es == nil { + return SF_DONE + } + + // Read pin state + pinHigh := MustGPIO().ReadPin(es.Pin) + + // Check if pin still matches expected trigger state + expectHigh := (es.Flags & ESF_PIN_HIGH) != 0 + triggered := (pinHigh && expectHigh) || (!pinHigh && !expectHigh) + + if !triggered { + // No longer matching - reschedule for the next attempt + t.Handler = endstopEvent + t.WakeTime = es.NextWake + es.TriggerCount = es.SampleCount + return SF_RESCHEDULE + } + + // Decrement trigger count + count := es.TriggerCount - 1 + if count == 0 { + // All samples confirmed - trigger! + if es.TriggerSync != nil { + TriggerSyncDoTrigger(es.TriggerSync, es.TriggerReason) + } + return SF_DONE + } + + // Continue oversampling + es.TriggerCount = count + t.WakeTime += es.SampleTime + return SF_RESCHEDULE +} diff --git a/core/endstop_analog.go b/core/endstop_analog.go new file mode 100644 index 0000000..f78d587 --- /dev/null +++ b/core/endstop_analog.go @@ -0,0 +1,317 @@ +// Analog endstop handling for ADC-based sensors +// Supports hall effect sensors, pressure sensors, and other analog sensors +package core + +import ( + "gopper/protocol" +) + +// AnalogEndstop represents a configured ADC-based endstop +type AnalogEndstop struct { + OID uint8 // Object ID + ADC *AnalogIn // ADC instance for reading analog values + Flags uint8 // State flags (ESF_*) + Timer Timer // Timer for sampling + SampleTime uint32 // Time between samples (in ticks) + SampleCount uint8 // Number of consecutive samples required + TriggerCount uint8 // Remaining samples before trigger + RestTime uint32 // Rest time between check cycles + NextWake uint32 // Next scheduled wake time + TriggerSync *TriggerSync // Associated trigger synchronization object + TriggerReason uint8 // Reason code to report when triggered + + // Analog-specific parameters + Threshold uint32 // Trigger threshold value (ADC counts) + TriggerAbove bool // True if trigger when value > threshold, false if value < threshold + Hysteresis uint32 // Hysteresis value to prevent oscillation +} + +// Global registry of analog endstops +var analogEndstops = make(map[uint8]*AnalogEndstop) + +// InitAnalogEndstopCommands registers analog endstop-related commands +func InitAnalogEndstopCommands() { + // Command to configure an analog endstop + RegisterCommand("config_analog_endstop", "oid=%c adc_oid=%c threshold=%u trigger_above=%c hysteresis=%u", handleConfigAnalogEndstop) + + // Command to start homing with an analog endstop + RegisterCommand("analog_endstop_home", "oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u trsync_oid=%c trigger_reason=%c", handleAnalogEndstopHome) + + // Command to query analog endstop state + RegisterCommand("analog_endstop_query_state", "oid=%c", handleAnalogEndstopQueryState) + + // Response: analog endstop state report + RegisterResponse("analog_endstop_state", "oid=%c homing=%c next_clock=%u value=%u") +} + +// handleConfigAnalogEndstop configures an analog endstop +// Format: config_analog_endstop oid=%c adc_oid=%c threshold=%u trigger_above=%c hysteresis=%u +func handleConfigAnalogEndstop(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + adcOID, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + threshold, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + triggerAbove, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + hysteresis, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get ADC object + adc, exists := GetADC(uint8(adcOID)) + if !exists { + return nil // Silently ignore if ADC not configured + } + + // Create new analog endstop instance + aes := &AnalogEndstop{ + OID: uint8(oid), + ADC: adc, + Threshold: threshold, + TriggerAbove: triggerAbove != 0, + Hysteresis: hysteresis, + } + + // Register in global map + analogEndstops[uint8(oid)] = aes + + return nil +} + +// handleAnalogEndstopHome starts homing with an analog endstop +// Format: analog_endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u trsync_oid=%c trigger_reason=%c +func handleAnalogEndstopHome(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + clock, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sampleTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sampleCount, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + restTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + trsyncOID, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + triggerReason, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get analog endstop object + aes, exists := analogEndstops[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Cancel any existing timer + aes.Timer.Next = nil + + // If sample_count is 0, disable homing + if sampleCount == 0 { + aes.TriggerSync = nil + aes.Flags = 0 + return nil + } + + // Get trigger sync object + ts, exists := GetTriggerSync(uint8(trsyncOID)) + if !exists { + return nil // Silently ignore if trsync not configured + } + + // Configure homing parameters + aes.SampleTime = sampleTicks + aes.SampleCount = uint8(sampleCount) + aes.TriggerCount = uint8(sampleCount) + aes.RestTime = restTicks + aes.TriggerSync = ts + aes.TriggerReason = uint8(triggerReason) + aes.Flags = ESF_HOMING + + // Schedule initial timer + aes.Timer.WakeTime = clock + aes.Timer.Handler = analogEndstopEvent + ScheduleTimer(&aes.Timer) + + return nil +} + +// handleAnalogEndstopQueryState queries the current analog endstop state +// Format: analog_endstop_query_state oid=%c +func handleAnalogEndstopQueryState(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get analog endstop object + aes, exists := analogEndstops[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Read current state + state := disableInterrupts() + eflags := aes.Flags + nextwake := aes.NextWake + restoreInterrupts(state) + + // Read ADC value + value := uint32(0) + if aes.ADC != nil { + value = uint32(aes.ADC.PendingValue) + } + + // Send response + homing := uint32(0) + if (eflags & ESF_HOMING) != 0 { + homing = 1 + } + + SendResponse("analog_endstop_state", func(output protocol.OutputBuffer) { + protocol.EncodeVLQUint(output, uint32(oid)) + protocol.EncodeVLQUint(output, homing) + protocol.EncodeVLQUint(output, nextwake) + protocol.EncodeVLQUint(output, value) + }) + + return nil +} + +// analogEndstopEvent is the timer callback for analog endstop checking +func analogEndstopEvent(t *Timer) uint8 { + // Find the AnalogEndstop instance that owns this timer + var aes *AnalogEndstop + for _, aesPtr := range analogEndstops { + if aesPtr != nil && &aesPtr.Timer == t { + aes = aesPtr + break + } + } + + if aes == nil { + return SF_DONE + } + + // Read ADC value + if aes.ADC == nil { + return SF_DONE + } + + value := uint32(aes.ADC.PendingValue) + + // Check if value crosses threshold + var triggered bool + if aes.TriggerAbove { + // Trigger when value rises above threshold + triggered = value > aes.Threshold + } else { + // Trigger when value falls below threshold + triggered = value < aes.Threshold + } + + nextWake := t.WakeTime + aes.RestTime + + if !triggered { + // No match - reschedule for the next attempt + t.WakeTime = nextWake + return SF_RESCHEDULE + } + + // Potential trigger detected - start oversampling + aes.NextWake = nextWake + t.Handler = analogEndstopOversampleEvent + return analogEndstopOversampleEvent(t) +} + +// analogEndstopOversampleEvent is the timer callback for oversampling +func analogEndstopOversampleEvent(t *Timer) uint8 { + // Find the AnalogEndstop instance that owns this timer + var aes *AnalogEndstop + for _, aesPtr := range analogEndstops { + if aesPtr != nil && &aesPtr.Timer == t { + aes = aesPtr + break + } + } + + if aes == nil { + return SF_DONE + } + + // Read ADC value + if aes.ADC == nil { + return SF_DONE + } + + value := uint32(aes.ADC.PendingValue) + + // Check if value still crosses threshold (with hysteresis) + var triggered bool + if aes.TriggerAbove { + // Trigger when value rises above threshold + // Use hysteresis to prevent oscillation + triggered = value > (aes.Threshold - aes.Hysteresis) + } else { + // Trigger when value falls below threshold + // Use hysteresis to prevent oscillation + triggered = value < (aes.Threshold + aes.Hysteresis) + } + + if !triggered { + // No longer matching - reschedule for the next attempt + t.Handler = analogEndstopEvent + t.WakeTime = aes.NextWake + aes.TriggerCount = aes.SampleCount + return SF_RESCHEDULE + } + + // Decrement trigger count + count := aes.TriggerCount - 1 + if count == 0 { + // All samples confirmed - trigger! + if aes.TriggerSync != nil { + TriggerSyncDoTrigger(aes.TriggerSync, aes.TriggerReason) + } + return SF_DONE + } + + // Continue oversampling + aes.TriggerCount = count + t.WakeTime += aes.SampleTime + return SF_RESCHEDULE +} diff --git a/core/endstop_i2c.go b/core/endstop_i2c.go new file mode 100644 index 0000000..943d6dc --- /dev/null +++ b/core/endstop_i2c.go @@ -0,0 +1,459 @@ +// I2C endstop handling for Time-of-Flight (TOF) and other I2C-based sensors +// Supports VL53L0X, VL53L1X, VL53L4CD, and similar distance sensors +package core + +import ( + "gopper/protocol" +) + +// I2CEndstop represents a configured I2C-based endstop +type I2CEndstop struct { + OID uint8 // Object ID + I2C *I2CDevice // I2C instance for communication + I2CAddr uint8 // I2C device address + Flags uint8 // State flags (ESF_*) + Timer Timer // Timer for sampling + SampleTime uint32 // Time between samples (in ticks) + SampleCount uint8 // Number of consecutive samples required + TriggerCount uint8 // Remaining samples before trigger + RestTime uint32 // Rest time between check cycles + NextWake uint32 // Next scheduled wake time + TriggerSync *TriggerSync // Associated trigger synchronization object + TriggerReason uint8 // Reason code to report when triggered + + // I2C-specific parameters + SensorType uint8 // Sensor type (VL53L0X, VL53L1X, etc.) + DistanceThreshold uint32 // Trigger distance threshold (in mm) + TriggerBelow bool // True if trigger when distance < threshold + Hysteresis uint32 // Hysteresis value to prevent oscillation (in mm) + + // Sensor state + LastDistance uint32 // Last measured distance (in mm) + Initialized bool // Sensor initialized flag +} + +// I2C Endstop sensor types +const ( + I2C_ENDSTOP_VL53L0X = 0x00 + I2C_ENDSTOP_VL53L1X = 0x01 + I2C_ENDSTOP_VL53L4CD = 0x02 +) + +// Global registry of I2C endstops +var i2cEndstops = make(map[uint8]*I2CEndstop) + +// InitI2CEndstopCommands registers I2C endstop-related commands +func InitI2CEndstopCommands() { + // Command to configure an I2C endstop + RegisterCommand("config_i2c_endstop", "oid=%c i2c_oid=%c addr=%c sensor_type=%c distance_threshold=%u trigger_below=%c hysteresis=%u", handleConfigI2CEndstop) + + // Command to start homing with an I2C endstop + RegisterCommand("i2c_endstop_home", "oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u trsync_oid=%c trigger_reason=%c", handleI2CEndstopHome) + + // Command to query I2C endstop state + RegisterCommand("i2c_endstop_query_state", "oid=%c", handleI2CEndstopQueryState) + + // Response: I2C endstop state report + RegisterResponse("i2c_endstop_state", "oid=%c homing=%c next_clock=%u distance=%u") +} + +// handleConfigI2CEndstop configures an I2C endstop +// Format: config_i2c_endstop oid=%c i2c_oid=%c addr=%c sensor_type=%c distance_threshold=%u trigger_below=%c hysteresis=%u +func handleConfigI2CEndstop(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + i2cOID, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + addr, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sensorType, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + distanceThreshold, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + triggerBelow, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + hysteresis, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get I2C object + i2c, exists := GetI2C(uint8(i2cOID)) + if !exists { + return nil // Silently ignore if I2C not configured + } + + // Create new I2C endstop instance + ies := &I2CEndstop{ + OID: uint8(oid), + I2C: i2c, + I2CAddr: uint8(addr), + SensorType: uint8(sensorType), + DistanceThreshold: distanceThreshold, + TriggerBelow: triggerBelow != 0, + Hysteresis: hysteresis, + Initialized: false, + } + + // Initialize the sensor based on type + if err := initializeI2CSensor(ies); err != nil { + // Log error but continue - sensor may be initialized later + } + + // Register in global map + i2cEndstops[uint8(oid)] = ies + + return nil +} + +// handleI2CEndstopHome starts homing with an I2C endstop +// Format: i2c_endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u trsync_oid=%c trigger_reason=%c +func handleI2CEndstopHome(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + clock, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sampleTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + sampleCount, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + restTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + trsyncOID, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + triggerReason, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get I2C endstop object + ies, exists := i2cEndstops[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Cancel any existing timer + ies.Timer.Next = nil + + // If sample_count is 0, disable homing + if sampleCount == 0 { + ies.TriggerSync = nil + ies.Flags = 0 + return nil + } + + // Get trigger sync object + ts, exists := GetTriggerSync(uint8(trsyncOID)) + if !exists { + return nil // Silently ignore if trsync not configured + } + + // Ensure sensor is initialized + if !ies.Initialized { + if err := initializeI2CSensor(ies); err != nil { + return err + } + } + + // Configure homing parameters + ies.SampleTime = sampleTicks + ies.SampleCount = uint8(sampleCount) + ies.TriggerCount = uint8(sampleCount) + ies.RestTime = restTicks + ies.TriggerSync = ts + ies.TriggerReason = uint8(triggerReason) + ies.Flags = ESF_HOMING + + // Schedule initial timer + ies.Timer.WakeTime = clock + ies.Timer.Handler = i2cEndstopEvent + ScheduleTimer(&ies.Timer) + + return nil +} + +// handleI2CEndstopQueryState queries the current I2C endstop state +// Format: i2c_endstop_query_state oid=%c +func handleI2CEndstopQueryState(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get I2C endstop object + ies, exists := i2cEndstops[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Read current state + state := disableInterrupts() + eflags := ies.Flags + nextwake := ies.NextWake + distance := ies.LastDistance + restoreInterrupts(state) + + // Send response + homing := uint32(0) + if (eflags & ESF_HOMING) != 0 { + homing = 1 + } + + SendResponse("i2c_endstop_state", func(output protocol.OutputBuffer) { + protocol.EncodeVLQUint(output, uint32(oid)) + protocol.EncodeVLQUint(output, homing) + protocol.EncodeVLQUint(output, nextwake) + protocol.EncodeVLQUint(output, distance) + }) + + return nil +} + +// initializeI2CSensor initializes the I2C sensor based on its type +func initializeI2CSensor(ies *I2CEndstop) error { + if ies.I2C == nil { + return nil + } + + // Initialize based on sensor type + switch ies.SensorType { + case I2C_ENDSTOP_VL53L0X: + // VL53L0X initialization sequence + // This is a simplified version - full initialization would require more steps + // Set continuous ranging mode + // Write to SYSRANGE_START register (0x00) with value 0x02 + if err := i2cWrite(ies.I2C, ies.I2CAddr, []byte{0x00, 0x02}); err != nil { + return err + } + + case I2C_ENDSTOP_VL53L1X: + // VL53L1X initialization sequence + // Set distance mode and timing budget + // This is a simplified version + if err := i2cWrite(ies.I2C, ies.I2CAddr, []byte{0x00, 0x01}); err != nil { + return err + } + + case I2C_ENDSTOP_VL53L4CD: + // VL53L4CD initialization sequence + // This is a simplified version + if err := i2cWrite(ies.I2C, ies.I2CAddr, []byte{0x00, 0x01}); err != nil { + return err + } + } + + ies.Initialized = true + return nil +} + +// readI2CDistance reads distance from the I2C sensor +func readI2CDistance(ies *I2CEndstop) (uint32, error) { + if ies.I2C == nil { + return 0, nil + } + + var distance uint32 + + switch ies.SensorType { + case I2C_ENDSTOP_VL53L0X: + // VL53L0X: Read from RESULT_RANGE_STATUS (0x14) + // Distance is at offset 10-11 (2 bytes, big-endian) + buf := make([]byte, 12) + if err := i2cRead(ies.I2C, ies.I2CAddr, 0x14, buf); err != nil { + return 0, err + } + // Extract distance (bytes 10-11) + distance = uint32(buf[10])<<8 | uint32(buf[11]) + + case I2C_ENDSTOP_VL53L1X: + // VL53L1X: Read from RESULT__FINAL_CROSSTALK_CORRECTED_RANGE_MM_SD0 (0x0096) + buf := make([]byte, 2) + if err := i2cRead(ies.I2C, ies.I2CAddr, 0x0096, buf); err != nil { + return 0, err + } + distance = uint32(buf[0])<<8 | uint32(buf[1]) + + case I2C_ENDSTOP_VL53L4CD: + // VL53L4CD: Similar to VL53L1X + buf := make([]byte, 2) + if err := i2cRead(ies.I2C, ies.I2CAddr, 0x0096, buf); err != nil { + return 0, err + } + distance = uint32(buf[0])<<8 | uint32(buf[1]) + } + + return distance, nil +} + +// i2cEndstopEvent is the timer callback for I2C endstop checking +func i2cEndstopEvent(t *Timer) uint8 { + // Find the I2CEndstop instance that owns this timer + var ies *I2CEndstop + for _, iesPtr := range i2cEndstops { + if iesPtr != nil && &iesPtr.Timer == t { + ies = iesPtr + break + } + } + + if ies == nil { + return SF_DONE + } + + // Read distance from sensor + distance, err := readI2CDistance(ies) + if err != nil { + // On error, reschedule and try again + t.WakeTime = t.WakeTime + ies.RestTime + return SF_RESCHEDULE + } + + ies.LastDistance = distance + + // Check if distance crosses threshold + var triggered bool + if ies.TriggerBelow { + // Trigger when distance falls below threshold + triggered = distance < ies.DistanceThreshold + } else { + // Trigger when distance rises above threshold + triggered = distance > ies.DistanceThreshold + } + + nextWake := t.WakeTime + ies.RestTime + + if !triggered { + // No match - reschedule for the next attempt + t.WakeTime = nextWake + return SF_RESCHEDULE + } + + // Potential trigger detected - start oversampling + ies.NextWake = nextWake + t.Handler = i2cEndstopOversampleEvent + return i2cEndstopOversampleEvent(t) +} + +// i2cEndstopOversampleEvent is the timer callback for oversampling +func i2cEndstopOversampleEvent(t *Timer) uint8 { + // Find the I2CEndstop instance that owns this timer + var ies *I2CEndstop + for _, iesPtr := range i2cEndstops { + if iesPtr != nil && &iesPtr.Timer == t { + ies = iesPtr + break + } + } + + if ies == nil { + return SF_DONE + } + + // Read distance from sensor + distance, err := readI2CDistance(ies) + if err != nil { + // On error, go back to main event + t.Handler = i2cEndstopEvent + t.WakeTime = ies.NextWake + ies.TriggerCount = ies.SampleCount + return SF_RESCHEDULE + } + + ies.LastDistance = distance + + // Check if distance still crosses threshold (with hysteresis) + var triggered bool + if ies.TriggerBelow { + // Trigger when distance falls below threshold + // Use hysteresis to prevent oscillation + triggered = distance < (ies.DistanceThreshold + ies.Hysteresis) + } else { + // Trigger when distance rises above threshold + // Use hysteresis to prevent oscillation + triggered = distance > (ies.DistanceThreshold - ies.Hysteresis) + } + + if !triggered { + // No longer matching - reschedule for the next attempt + t.Handler = i2cEndstopEvent + t.WakeTime = ies.NextWake + ies.TriggerCount = ies.SampleCount + return SF_RESCHEDULE + } + + // Decrement trigger count + count := ies.TriggerCount - 1 + if count == 0 { + // All samples confirmed - trigger! + if ies.TriggerSync != nil { + TriggerSyncDoTrigger(ies.TriggerSync, ies.TriggerReason) + } + return SF_DONE + } + + // Continue oversampling + ies.TriggerCount = count + t.WakeTime += ies.SampleTime + return SF_RESCHEDULE +} + +// Helper functions for I2C communication + +func i2cWrite(i2c *I2CDevice, addr uint8, data []byte) error { + if i2c == nil { + return nil + } + // Use the I2C HAL to write data + return MustI2C().Write(i2c.Bus, I2CAddress(addr), data) +} + +func i2cRead(i2c *I2CDevice, addr uint8, reg uint8, buf []byte) error { + if i2c == nil { + return nil + } + // Use the I2C HAL combined write-then-read operation: + // reg is sent as regData, then readLen bytes are read into a temporary buffer. + readLen := uint8(len(buf)) + data, err := MustI2C().Read(i2c.Bus, I2CAddress(addr), []byte{reg}, readLen) + if err != nil { + return err + } + copy(buf, data) + return nil +} diff --git a/core/gpio_hal.go b/core/gpio_hal.go index 14c9e12..5d9684b 100644 --- a/core/gpio_hal.go +++ b/core/gpio_hal.go @@ -10,11 +10,20 @@ type GPIODriver interface { // Returns error if pin is invalid or already in use ConfigureOutput(pin GPIOPin) error + // ConfigureInputPullUp configures a pin as a digital input with pull-up resistor + ConfigureInputPullUp(pin GPIOPin) error + + // ConfigureInputPullDown configures a pin as a digital input with pull-down resistor + ConfigureInputPullDown(pin GPIOPin) error + // SetPin sets the pin to high (true) or low (false) SetPin(pin GPIOPin, value bool) error // GetPin reads the current pin state GetPin(pin GPIOPin) (bool, error) + + // ReadPin reads the current pin state (alias for GetPin for convenience) + ReadPin(pin GPIOPin) bool } // Global singleton used by core code. diff --git a/core/i2c.go b/core/i2c.go index e32c64b..2d8c4d4 100644 --- a/core/i2c.go +++ b/core/i2c.go @@ -19,6 +19,12 @@ type I2CDevice struct { // Global registry of I2C devices var i2cDevices = make(map[uint8]*I2CDevice) +// GetI2C retrieves an I2C device by OID +func GetI2C(oid uint8) (*I2CDevice, bool) { + device, exists := i2cDevices[oid] + return device, exists +} + // InitI2CCommands registers I2C-related commands with the command registry func InitI2CCommands() { // Command to allocate an I2C device object diff --git a/core/trsync.go b/core/trsync.go new file mode 100644 index 0000000..4b2cd52 --- /dev/null +++ b/core/trsync.go @@ -0,0 +1,264 @@ +// Trigger synchronization for multi-axis homing +// Implements Klipper's trsync protocol for coordinated endstop triggers +package core + +import ( + "gopper/protocol" +) + +// TriggerSync flags +const ( + TSF_CAN_TRIGGER = 1 << 0 // Trigger is enabled + TSF_TRIGGERED = 1 << 1 // Trigger has fired +) + +// TriggerSignal represents a callback registered with a TriggerSync +type TriggerSignal struct { + Callback func(reason uint8) // Called when trigger fires + Next *TriggerSignal +} + +// TriggerSync coordinates multiple endstops during homing +type TriggerSync struct { + OID uint8 // Object ID + Flags uint8 // State flags (TSF_*) + TriggerReason uint8 // Reason code for the trigger + ExpireReason uint8 // Reason code if timeout expires + ReportTicks uint32 // Interval for status reports + ReportTimer Timer // Timer for periodic reports + ExpireTimer Timer // Timer for timeout + Signals *TriggerSignal // Linked list of registered callbacks +} + +// Global registry of trigger sync objects +var triggerSyncs = make(map[uint8]*TriggerSync) + +// InitTriggerSyncCommands registers trsync-related commands +func InitTriggerSyncCommands() { + // Command to set timeout for trigger synchronization + RegisterCommand("trsync_start", "oid=%c report_clock=%u report_ticks=%u expire_reason=%c", handleTriggerSyncStart) + + // Command to set timeout for a trigger sync + RegisterCommand("trsync_set_timeout", "oid=%c clock=%u", handleTriggerSyncSetTimeout) + + // Command to manually trigger a trsync + RegisterCommand("trsync_trigger", "oid=%c reason=%c", handleTriggerSyncTrigger) + + // Response: trsync report sent to host + RegisterResponse("trsync_state", "oid=%c can_trigger=%c trigger_reason=%c clock=%u") +} + +// handleTriggerSyncStart starts a trigger synchronization session +// Format: trsync_start oid=%c report_clock=%u report_ticks=%u expire_reason=%c +func handleTriggerSyncStart(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + reportClock, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + reportTicks, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + expireReason, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get or create trigger sync object + ts, exists := triggerSyncs[uint8(oid)] + if !exists { + ts = &TriggerSync{ + OID: uint8(oid), + } + triggerSyncs[uint8(oid)] = ts + } + + // Reset state + ts.Flags = TSF_CAN_TRIGGER + ts.TriggerReason = 0 + ts.ExpireReason = uint8(expireReason) + ts.ReportTicks = reportTicks + + // Schedule report timer + if reportTicks > 0 { + ts.ReportTimer.WakeTime = reportClock + ts.ReportTimer.Handler = triggerSyncReportEvent + ScheduleTimer(&ts.ReportTimer) + } + + return nil +} + +// handleTriggerSyncSetTimeout sets a timeout for trigger synchronization +// Format: trsync_set_timeout oid=%c clock=%u +func handleTriggerSyncSetTimeout(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + clock, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get trigger sync object + ts, exists := triggerSyncs[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Schedule expire timer + ts.ExpireTimer.WakeTime = clock + ts.ExpireTimer.Handler = triggerSyncExpireEvent + ScheduleTimer(&ts.ExpireTimer) + + return nil +} + +// handleTriggerSyncTrigger manually triggers a trsync +// Format: trsync_trigger oid=%c reason=%c +func handleTriggerSyncTrigger(data *[]byte) error { + oid, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + reason, err := protocol.DecodeVLQUint(data) + if err != nil { + return err + } + + // Get trigger sync object + ts, exists := triggerSyncs[uint8(oid)] + if !exists { + return nil // Silently ignore if not configured + } + + // Trigger it + TriggerSyncDoTrigger(ts, uint8(reason)) + + return nil +} + +// TriggerSyncDoTrigger fires a trigger synchronization event +// This is called by endstops when they detect a trigger condition +func TriggerSyncDoTrigger(ts *TriggerSync, reason uint8) { + state := disableInterrupts() + defer restoreInterrupts(state) + + // Check if we can trigger + if (ts.Flags & TSF_CAN_TRIGGER) == 0 { + return + } + + // Mark as triggered + ts.Flags &^= TSF_CAN_TRIGGER + ts.Flags |= TSF_TRIGGERED + ts.TriggerReason = reason + + // Call all registered signal callbacks + signal := ts.Signals + for signal != nil { + if signal.Callback != nil { + signal.Callback(reason) + } + signal = signal.Next + } +} + +// TriggerSyncAddSignal registers a callback with a trigger sync +func TriggerSyncAddSignal(ts *TriggerSync, callback func(reason uint8)) *TriggerSignal { + state := disableInterrupts() + defer restoreInterrupts(state) + + signal := &TriggerSignal{ + Callback: callback, + Next: ts.Signals, + } + ts.Signals = signal + + return signal +} + +// triggerSyncReportEvent is the timer handler for periodic status reports +func triggerSyncReportEvent(t *Timer) uint8 { + // Find the TriggerSync instance that owns this timer + var ts *TriggerSync + for _, tsPtr := range triggerSyncs { + if tsPtr != nil && &tsPtr.ReportTimer == t { + ts = tsPtr + break + } + } + + if ts == nil { + return SF_DONE + } + + // Send report to host + triggerSyncReport(ts) + + // Reschedule if still active + if (ts.Flags & TSF_CAN_TRIGGER) != 0 { + t.WakeTime = GetTime() + ts.ReportTicks + return SF_RESCHEDULE + } + + return SF_DONE +} + +// triggerSyncExpireEvent is the timer handler for timeout expiration +func triggerSyncExpireEvent(t *Timer) uint8 { + // Find the TriggerSync instance that owns this timer + var ts *TriggerSync + for _, tsPtr := range triggerSyncs { + if tsPtr != nil && &tsPtr.ExpireTimer == t { + ts = tsPtr + break + } + } + + if ts == nil { + return SF_DONE + } + + // Trigger with expire reason + TriggerSyncDoTrigger(ts, ts.ExpireReason) + + // Send final report + triggerSyncReport(ts) + + return SF_DONE +} + +// triggerSyncReport sends a status report to the host +func triggerSyncReport(ts *TriggerSync) { + canTrigger := uint32(0) + if (ts.Flags & TSF_CAN_TRIGGER) != 0 { + canTrigger = 1 + } + + clock := GetTime() + + // Send trsync_state response + SendResponse("trsync_state", func(output protocol.OutputBuffer) { + protocol.EncodeVLQUint(output, uint32(ts.OID)) + protocol.EncodeVLQUint(output, canTrigger) + protocol.EncodeVLQUint(output, uint32(ts.TriggerReason)) + protocol.EncodeVLQUint(output, clock) + }) +} + +// GetTriggerSync retrieves a trigger sync by OID +func GetTriggerSync(oid uint8) (*TriggerSync, bool) { + ts, exists := triggerSyncs[oid] + return ts, exists +} diff --git a/docs/endstop.md b/docs/endstop.md new file mode 100644 index 0000000..0a4a852 --- /dev/null +++ b/docs/endstop.md @@ -0,0 +1,260 @@ +# Endstop Implementation in Gopper + +This document describes the endstop implementation in Gopper, which provides comprehensive support for various types of endstop sensors used in 3D printers. + +## Overview + +Gopper implements Klipper-compatible endstop functionality with support for: +- **GPIO-based endstops** (mechanical switches, hall effect sensors with digital output) +- **Analog endstops** (ADC-based sensors, analog hall effect sensors) +- **I2C endstops** (Time-of-Flight sensors like VL53L0X, VL53L1X, VL53L4CD) + +## Architecture + +### Core Components + +1. **Trigger Synchronization (trsync)** (`core/trsync.go`) + - Coordinates multiple endstops during homing operations + - Manages trigger callbacks and timeouts + - Reports trigger events to the host + +2. **GPIO Endstops** (`core/endstop.go`) + - Traditional mechanical switches + - Hall effect sensors with digital output + - Optical sensors with digital output + - Uses timer-based sampling with oversampling to prevent false triggers + +3. **Analog Endstops** (`core/endstop_analog.go`) + - Hall effect sensors with analog output + - Pressure-sensitive sensors + - Threshold-based triggering with hysteresis + +4. **I2C Endstops** (`core/endstop_i2c.go`) + - Time-of-Flight (TOF) sensors (VL53L0X, VL53L1X, VL53L4CD) + - Distance-based triggering with hysteresis + +## Command Protocol + +### Trigger Synchronization Commands + +#### `trsync_start` +Format: `trsync_start oid=%c report_clock=%u report_ticks=%u expire_reason=%c` + +Starts a trigger synchronization session for coordinated homing. + +Parameters: +- `oid`: Object ID of the trigger sync object +- `report_clock`: Initial clock time for status reports +- `report_ticks`: Interval between status reports (in timer ticks) +- `expire_reason`: Reason code to report if timeout expires + +#### `trsync_set_timeout` +Format: `trsync_set_timeout oid=%c clock=%u` + +Sets a timeout for the trigger synchronization. + +Parameters: +- `oid`: Object ID of the trigger sync object +- `clock`: Clock time when timeout expires + +#### `trsync_trigger` +Format: `trsync_trigger oid=%c reason=%c` + +Manually triggers a trsync object. + +Parameters: +- `oid`: Object ID of the trigger sync object +- `reason`: Reason code for the trigger + +#### Response: `trsync_state` +Format: `trsync_state oid=%c can_trigger=%c trigger_reason=%c clock=%u` + +Reports the current state of a trigger sync object. + +### GPIO Endstop Commands + +#### `config_endstop` +Format: `config_endstop oid=%c pin=%u pull_up=%c` + +Configures a GPIO pin as an endstop input. + +Parameters: +- `oid`: Object ID for the endstop +- `pin`: GPIO pin number +- `pull_up`: 1 to enable pull-up resistor, 0 for pull-down + +#### `endstop_home` +Format: `endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u pin_value=%c trsync_oid=%c trigger_reason=%c` + +Starts homing with a GPIO endstop. + +Parameters: +- `oid`: Object ID of the endstop +- `clock`: Clock time to start checking +- `sample_ticks`: Time between consecutive samples during oversampling +- `sample_count`: Number of consecutive samples required to confirm trigger (0 to disable) +- `rest_ticks`: Time between check cycles +- `pin_value`: Expected pin value when triggered (1=high, 0=low) +- `trsync_oid`: Object ID of the associated trigger sync +- `trigger_reason`: Reason code to report when triggered + +#### `endstop_query_state` +Format: `endstop_query_state oid=%c` + +Queries the current state of an endstop. + +#### Response: `endstop_state` +Format: `endstop_state oid=%c homing=%c next_clock=%u pin_value=%c` + +Reports the current state of a GPIO endstop. + +### Analog Endstop Commands + +#### `config_analog_endstop` +Format: `config_analog_endstop oid=%c adc_oid=%c threshold=%u trigger_above=%c hysteresis=%u` + +Configures an analog (ADC-based) endstop. + +Parameters: +- `oid`: Object ID for the endstop +- `adc_oid`: Object ID of the associated ADC channel +- `threshold`: ADC value threshold for triggering +- `trigger_above`: 1 to trigger when value > threshold, 0 when value < threshold +- `hysteresis`: Hysteresis value to prevent oscillation + +#### `analog_endstop_home` +Format: `analog_endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u trsync_oid=%c trigger_reason=%c` + +Starts homing with an analog endstop. + +#### `analog_endstop_query_state` +Format: `analog_endstop_query_state oid=%c` + +Queries the current state of an analog endstop. + +#### Response: `analog_endstop_state` +Format: `analog_endstop_state oid=%c homing=%c next_clock=%u value=%u` + +Reports the current state of an analog endstop, including the latest ADC value. + +### I2C Endstop Commands + +#### `config_i2c_endstop` +Format: `config_i2c_endstop oid=%c i2c_oid=%c addr=%c sensor_type=%c distance_threshold=%u trigger_below=%c hysteresis=%u` + +Configures an I2C-based endstop (e.g., TOF sensor). + +Parameters: +- `oid`: Object ID for the endstop +- `i2c_oid`: Object ID of the associated I2C device +- `addr`: I2C device address +- `sensor_type`: Sensor type (0=VL53L0X, 1=VL53L1X, 2=VL53L4CD) +- `distance_threshold`: Distance threshold for triggering (in mm) +- `trigger_below`: 1 to trigger when distance < threshold, 0 when distance > threshold +- `hysteresis`: Hysteresis value to prevent oscillation (in mm) + +#### `i2c_endstop_home` +Format: `i2c_endstop_home oid=%c clock=%u sample_ticks=%u sample_count=%c rest_ticks=%u trsync_oid=%c trigger_reason=%c` + +Starts homing with an I2C endstop. + +#### `i2c_endstop_query_state` +Format: `i2c_endstop_query_state oid=%c` + +Queries the current state of an I2C endstop. + +#### Response: `i2c_endstop_state` +Format: `i2c_endstop_state oid=%c homing=%c next_clock=%u distance=%u` + +Reports the current state of an I2C endstop, including the latest distance reading (in mm). + +## Implementation Details + +### Oversampling for Noise Rejection + +All endstop types implement a two-stage detection mechanism to prevent false triggers: + +1. **Initial Detection**: The endstop is checked periodically (every `rest_ticks`) +2. **Oversampling**: When a potential trigger is detected, the endstop is sampled multiple times consecutively (every `sample_ticks`) to confirm the trigger + +This approach, borrowed from Klipper, prevents false triggers caused by electrical noise or mechanical bounce. + +### Trigger Synchronization + +The `trsync` system coordinates multiple endstops during homing operations: + +1. Multiple endstops can be registered with the same `trsync` object +2. When any endstop triggers, all registered callbacks are invoked +3. The first trigger wins - subsequent triggers are ignored +4. Timeout mechanism provides fallback if no endstop triggers + +### Platform Support + +The endstop implementation is platform-agnostic and relies on HAL (Hardware Abstraction Layer) interfaces: + +- **GPIO HAL**: Provides pin configuration and reading +- **ADC HAL**: Provides analog-to-digital conversion +- **I2C HAL**: Provides I2C communication + +Currently supported platforms: +- RP2040 (Raspberry Pi Pico) +- RP2350 (Raspberry Pi Pico 2) + +## Usage Examples + +### Basic Mechanical Switch + +```python +# Klipper configuration example +[stepper_x] +endstop_pin: ^gpio25 # Pull-up enabled +``` + +The firmware will: +1. Configure GPIO25 as input with pull-up +2. During homing, sample the pin multiple times to confirm trigger +3. Report trigger to the host via trsync + +### Hall Effect Sensor (Analog) + +```python +# Klipper configuration example +[stepper_y] +endstop_pin: analog_endstop:ADC0 +``` + +The firmware will: +1. Configure ADC0 for analog sampling +2. Monitor ADC value against threshold +3. Use hysteresis to prevent oscillation +4. Report trigger when threshold is crossed consistently + +### TOF Sensor (I2C) + +```python +# Klipper configuration example +[stepper_z] +endstop_pin: i2c_endstop:VL53L0X +``` + +The firmware will: +1. Initialize VL53L0X sensor via I2C +2. Periodically read distance measurements +3. Trigger when distance crosses threshold +4. Use hysteresis to prevent oscillation + +## Future Enhancements + +Potential improvements for future releases: + +1. **Sensorless Homing**: Detect motor stall current for endstop detection +2. **Encoder-based Endstops**: Use rotary encoders for position detection +3. **Multiple Sensor Fusion**: Combine data from multiple sensor types +4. **Dynamic Threshold Adjustment**: Auto-tune thresholds based on environmental conditions +5. **Advanced Filtering**: Implement Kalman filtering for noisy sensors + +## References + +- Klipper endstop implementation: [src/endstop.c](https://github.com/Klipper3d/klipper/blob/master/src/endstop.c) +- Klipper trsync implementation: [src/trsync.c](https://github.com/Klipper3d/klipper/blob/master/src/trsync.c) +- Klipper endstop phase: [docs/Endstop_Phase.md](https://github.com/Klipper3d/klipper/blob/master/docs/Endstop_Phase.md) diff --git a/targets/rp2040/gpio.go b/targets/rp2040/gpio.go index bfa5f8b..4767695 100644 --- a/targets/rp2040/gpio.go +++ b/targets/rp2040/gpio.go @@ -13,6 +13,50 @@ type RPGPIODriver struct { configuredPins map[core.GPIOPin]machine.Pin } +func (d *RPGPIODriver) ConfigureInputPullUp(pin core.GPIOPin) error { + // Check if already configured + if _, exists := d.configuredPins[pin]; exists { + // Already configured, this is OK + return nil + } + + // Map pin to machine.Pin + machinePin := d.pinNumberToMachinePin(pin) + + // Configure as input with pull-up resistor + machinePin.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + // Track configured pin + d.configuredPins[pin] = machinePin + + return nil +} + +func (d *RPGPIODriver) ConfigureInputPullDown(pin core.GPIOPin) error { + // Check if already configured + if _, exists := d.configuredPins[pin]; exists { + // Already configured, this is OK + return nil + } + + // Map pin to machine.Pin + machinePin := d.pinNumberToMachinePin(pin) + + // Configure as input with pull-down resistor + machinePin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) + + // Track configured pin + d.configuredPins[pin] = machinePin + + return nil +} + +func (d *RPGPIODriver) ReadPin(pin core.GPIOPin) bool { + // ReadPin is a convenience wrapper around GetPin that returns just the bool value + value, _ := d.GetPin(pin) + return value +} + // NewRPGPIODriver creates a new RP2040 GPIO driver func NewRPGPIODriver() *RPGPIODriver { return &RPGPIODriver{