diff --git a/.github/workflows/ci-build_test.yaml b/.github/workflows/ci-build_test.yaml index 885af867b..05a827591 100644 --- a/.github/workflows/ci-build_test.yaml +++ b/.github/workflows/ci-build_test.yaml @@ -3,7 +3,7 @@ name: Build test on: workflow_dispatch: - push: + pull_request: jobs: build: @@ -27,8 +27,8 @@ jobs: - name: Test Klippy Only if: steps.changes.outputs.klippy == 'true' && steps.changes.outputs.klipper == 'false' - run: docker run -v $PWD:/klipper ${{ secrets.DOCKERHUB_USERNAME }}/klipper-build:latest "./scripts/ci-build.sh" 2>&1 + run: docker run -v $PWD:/klipper dangerklippers/klipper-build:latest "./scripts/ci-build.sh" 2>&1 - name: Test Klipper Full if: steps.changes.outputs.klippy == 'true' && steps.changes.outputs.klipper == 'true' - run: docker run -v $PWD:/klipper ${{ secrets.DOCKERHUB_USERNAME }}/klipper-build:latest "./scripts/ci-build.sh" compile 2>&1 + run: docker run -v $PWD:/klipper dangerklippers/klipper-build:latest "./scripts/ci-build.sh" compile 2>&1 diff --git a/.github/workflows/ci-builder.yaml b/.github/workflows/ci-builder.yaml index 1f290a786..14b8f2d4d 100644 --- a/.github/workflows/ci-builder.yaml +++ b/.github/workflows/ci-builder.yaml @@ -20,7 +20,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build - run: docker build -f scripts/Dockerfile-build -t ${{ secrets.DOCKERHUB_USERNAME }}/klipper-build:latest . + run: docker build -f scripts/Dockerfile-build -t dangerklippers/klipper-build:latest . - name: Push - run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/klipper-build:latest + run: docker push dangerklippers/klipper-build:latest diff --git a/README.md b/README.md index f5fbd7d2f..28d6008f4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ If I want my printer to light itself on fire, I should be able to make my printe - [danger_options: option to configure the homing elapsed distance tolerance](https://github.com/DangerKlippers/danger-klipper/pull/110) +- [danger_options: option to ignore ADC out of range](https://github.com/DangerKlippers/danger-klipper/pull/129) + - [temperature_mcu: add reference_voltage](https://github.com/DangerKlippers/danger-klipper/pull/99) ([klipper#5713](https://github.com/Klipper3d/klipper/pull/5713)) - [stepper: current_change_dwell_time](https://github.com/DangerKlippers/danger-klipper/pull/90) @@ -62,6 +64,8 @@ If I want my printer to light itself on fire, I should be able to make my printe - [tla2518 support](https://github.com/DangerKlippers/danger-klipper/pull/103) +- [adxl345: improve ACCELEROMETER_QUERY command](https://github.com/DangerKlippers/danger-klipper/pull/124) + If you're feeling adventurous, take a peek at the extra features in the bleeding-edge branch: - [dmbutyugin's advanced-features branch](https://github.com/DangerKlippers/danger-klipper/pull/69) [dmbutyugin/advanced-features](https://github.com/dmbutyugin/klipper/commits/advanced-features) diff --git a/config/generic-I3DBEEZ9.cfg b/config/generic-I3DBEEZ9.cfg new file mode 100644 index 000000000..abb20d86f --- /dev/null +++ b/config/generic-I3DBEEZ9.cfg @@ -0,0 +1,223 @@ +# This file contains common pin mappings for the I3DBEEZ9 V1.0. +# To use this config, the firmware should be compiled for the +# STM32F407 with a "32KiB bootloader". + +# The "make flash" command does not work on the I3DBEEZ9. Instead, +# after running "make", copy the generated "out/klipper.bin" file to a +# file named "firmware.bin" on an SD card and then restart the I3DBEEZ9 +# with that SD card. + +# See docs/Config_Reference.md for a description of parameters. + +[stepper_x] +step_pin: PE9 +dir_pin: PF1 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: PB10 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PE11 +dir_pin: PE1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: PE12 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PE13 +dir_pin: PC2 +enable_pin: !PC0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: PG8 +position_endstop: 0 +position_max: 200 + +[extruder] +step_pin: PE14 +dir_pin: PA0 +enable_pin: !PC3 +microsteps: 16 +rotation_distance: 33.500 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PB1 # Heat0 +sensor_pin: PF4 # T1 Header +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: PD15 +#dir_pin: PE7 +#enable_pin: !PA3 +#heater_pin: PD14 # Heat1 +#sensor_pin: PF5 # T2 +#... + +#[extruder2] +#step_pin: PD13 +#dir_pin: PG9 +#enable_pin: !PF0 +#heater_pin: PB0 # Heat2 +#sensor_pin: PF6 # T3 +#... + +#[stepper_z1] +#step_pin: PE4 +#dir_pin: PE3 +#enable_pin: !PC13 +#microsteps: 16 +#rotation_distance: 8 +#endstop_pin: PD0 +#position_endstop: 0.5 +#position_max: 200 + +[heater_bed] +heater_pin: PD12 +sensor_pin: PF3 # T0 +sensor_type: ATC Semitec 104GT-2 +control: watermark +min_temp: 0 +max_temp: 130 + +[fan] +pin: PC8 + +[heater_fan fan1] +pin: PE5 + +#[heater_fan fan2] +#pin: PE6 + +[mcu] +serial: /dev/serial/by-id/usb-Klipper_Klipper_firmware_12345-if00 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 + + +######################################## +# TMC2208 configuration +######################################## + +#[tmc2208 stepper_x] +#uart_pin: PA15 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2208 stepper_y] +#uart_pin: PB8 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2208 stepper_z] +#uart_pin: PB9 +#run_current: 0.650 +#stealthchop_threshold: 999999 + +#[tmc2208 extruder] +#uart_pin: PB3 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2208 extruder1] +#uart_pin: PG15 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2208 extruder2] +#uart_pin: PG12 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2208 stepper_z1] +#uart_pin: PE2 +#run_current: 0.650 +#stealthchop_threshold: 999999 + +######################################## +# TMC2130 configuration +######################################## + +#[tmc2130 stepper_x] +#cs_pin: PA15 +#spi_bus: spi3a +##diag1_pin: PB10 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2130 stepper_y] +#cs_pin: PB8 +#spi_bus: spi3a +##diag1_pin: PE12 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2130 stepper_z] +#cs_pin: PB9 +#spi_bus: spi3a +##diag1_pin: PG8 +#run_current: 0.650 +#stealthchop_threshold: 999999 + +#[tmc2130 extruder] +#cs_pin: PB3 +#spi_bus: spi3a +##diag1_pin: PE15 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2130 extruder1] +#cs_pin: PG15 +#spi_bus: spi3a +##diag1_pin: PE10 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2130 extruder2] +#cs_pin: PG12 +#spi_bus: spi3a +##diag1_pin: PG5 +#run_current: 0.800 +#stealthchop_threshold: 999999 + +#[tmc2130 stepper_z1] +#cs_pin: PE2 +#spi_bus: spi3a +##diag1_pin: PD0 +#run_current: 0.650 +#stealthchop_threshold: 999999 + + +######################################## +# EXP1 / EXP2 (display) pins +######################################## + +[board_pins] +aliases: + # EXP1 header + EXP1_1=PG4, EXP1_3=PD11, EXP1_5=PG2, EXP1_7=PG6, EXP1_9=, + EXP1_2=PA8, EXP1_4=PD10, EXP1_6=PG3, EXP1_8=PG7, EXP1_10=<5V>, + # EXP2 header + EXP2_1=PB14, EXP2_3=PG10, EXP2_5=PF11, EXP2_7=PF12, EXP2_9=, + EXP2_2=PB13, EXP2_4=PB12, EXP2_6=PB15, EXP2_8=, EXP2_10=PF13 + # Pins EXP2_1, EXP2_6, EXP2_2 are also MISO, MOSI, SCK of bus "spi2" + +# See the sample-lcd.cfg file for definitions of common LCD displays. diff --git a/config/generic-mini-rambo.cfg b/config/generic-mini-rambo.cfg index 61e2ac847..1a616cf80 100644 --- a/config/generic-mini-rambo.cfg +++ b/config/generic-mini-rambo.cfg @@ -84,7 +84,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_z_current] pin: PL4 @@ -92,7 +92,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_e_current] pin: PL5 @@ -100,7 +100,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.25 +value: 1.25 [static_digital_output stepper_config] pins: diff --git a/config/generic-ultimaker-ultimainboard-v2.cfg b/config/generic-ultimaker-ultimainboard-v2.cfg index 9a4d4e6da..b1ce3fa55 100644 --- a/config/generic-ultimaker-ultimainboard-v2.cfg +++ b/config/generic-ultimaker-ultimainboard-v2.cfg @@ -97,7 +97,7 @@ max_z_accel: 30 [output_pin case_light] pin: PH5 -static_value: 1.0 +value: 1.0 # Motor current settings. [output_pin stepper_xy_current] @@ -107,7 +107,7 @@ scale: 2.000 # Max power setting. cycle_time: .000030 hardware_pwm: True -static_value: 1.200 +value: 1.200 # Power adjustment setting. [output_pin stepper_z_current] @@ -116,7 +116,7 @@ pwm: True scale: 2.000 cycle_time: .000030 hardware_pwm: True -static_value: 1.200 +value: 1.200 [output_pin stepper_e_current] pin: PL3 @@ -124,4 +124,4 @@ pwm: True scale: 2.000 cycle_time: .000030 hardware_pwm: True -static_value: 1.250 +value: 1.250 diff --git a/config/printer-adimlab-2018.cfg b/config/printer-adimlab-2018.cfg index 2f02173dd..d810e9d7e 100644 --- a/config/printer-adimlab-2018.cfg +++ b/config/printer-adimlab-2018.cfg @@ -89,7 +89,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_z_current] pin: PL4 @@ -97,7 +97,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.3 +value: 1.3 [output_pin stepper_e_current] pin: PL3 @@ -105,7 +105,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.25 +value: 1.25 [display] lcd_type: st7920 diff --git a/config/printer-creality-cr30-2021.cfg b/config/printer-creality-cr30-2021.cfg index de9469200..1edc75313 100644 --- a/config/printer-creality-cr30-2021.cfg +++ b/config/printer-creality-cr30-2021.cfg @@ -98,7 +98,6 @@ max_temp: 100 [output_pin led] pin: PC14 -static_value: 0 # Neopixel LED support # [neopixel led_neopixel] diff --git a/config/printer-creality-ender5-s1-2023.cfg b/config/printer-creality-ender5-s1-2023.cfg new file mode 100644 index 000000000..68a89fa5e --- /dev/null +++ b/config/printer-creality-ender5-s1-2023.cfg @@ -0,0 +1,170 @@ +# Creality Ender 5 S1 (HW version: CR4NS200141C13) +# +# printer_size: 220x220x280 +# To use this config, during "make menuconfig" select the STM32F401 +# with a "64KiB bootloader" and serial (on USART1 PA10/PA9) +# communication. +# +# Flash this firmware by creating a directory named "STM32F4_UPDATE" +# on an SD card, copying the "out/klipper.bin" to it and then turn +# on the printer with the card inserted. The firmware filename must +# end in ".bin" and must not match the last filename that was flashed. +# +# See docs/Config_Reference.md for a description of parameters. + +[stepper_x] +step_pin: PC2 +dir_pin: !PB9 +enable_pin: !PC3 +rotation_distance: 40 +microsteps: 16 +endstop_pin: !PA5 +position_endstop: 220 +position_max: 222 +homing_speed: 80 + +[stepper_y] +step_pin: PB8 +dir_pin: !PB7 +enable_pin: !PC3 +rotation_distance: 40 +microsteps: 16 +endstop_pin: !PA6 +position_endstop: 220 +position_max: 220 +homing_speed: 80 + +[stepper_z] +step_pin: PB6 +dir_pin: PB5 +enable_pin: !PC3 +rotation_distance: 8 +microsteps: 16 +endstop_pin: probe:z_virtual_endstop +position_max: 280 +homing_speed: 20 +second_homing_speed: 1 +homing_retract_dist: 2.0 + +[extruder] +step_pin: PB4 +dir_pin: PB3 +enable_pin: !PC3 +rotation_distance: 7.5 +microsteps: 16 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PA1 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC5 +control: pid # tuned for stock hardware with 210 degree Celsius target +pid_kp: 20.749 +pid_ki: 1.064 +pid_kd: 101.153 +min_temp: 0 +max_temp: 305 + +[heater_bed] +heater_pin: PA7 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PC4 +control: pid # tuned for stock hardware with 60 degree Celsius target +pid_kp: 66.566 +pid_ki: 0.958 +pid_kd: 1155.761 +min_temp: 0 +max_temp: 110 + +# Part cooling fan +[fan] +pin: PA0 +kick_start_time: 0.5 + +# Hotend fan +# set fan runnig when extruder temperature is over 60 +[heater_fan heatbreak_fan] +pin: PC0 +heater:extruder +heater_temp: 60 +fan_speed: 0.8 + +[filament_switch_sensor filament_sensor] +pause_on_runout: true +switch_pin: ^!PC15 + +# Stock CR Touch bed sensor +[bltouch] +sensor_pin: ^PC14 +control_pin: PC13 +x_offset: -13 +y_offset: 27 +z_offset: 2.0 +speed: 10 +stow_on_each_sample: true # Occasional bed crashes when false +samples: 4 +sample_retract_dist: 2 +samples_result: average +probe_with_touch_mode: true + +[bed_mesh] +speed: 150 +mesh_min: 3,28 # need to handle head distance with bl_touch +mesh_max: 205,218 +mesh_pps: 3 +probe_count: 4,4 +fade_start: 1 +fade_end: 10 +fade_target: 0 + +[mcu] +serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 +restart_method: command + +[safe_z_home] +home_xy_position: 123,83 +speed: 200 +z_hop: 10 +z_hop_speed: 10 + +# Many Ender 5 S1 printers appear to suffer from a slight twist +# in the X axis. This twist can be measured, and compensated for +# using the AXIS_TWIST_COMPENSATION_CALIBRATE G-Code command. See +# https://www.klipper3d.org/Axis_Twist_Compensation.html for more +# information. This section provides the setup for this optional +# calibration step. +[axis_twist_compensation] +calibrate_start_x: 3 +calibrate_end_x: 207 +calibrate_y: 110 + +# Probe locations for assisted bed screw adjustment. +[screws_tilt_adjust] +screw1: 38,6 +screw1_name: Front Left Screw +screw2: 215,6 +screw2_name: Front Right Screw +screw3: 215,175 +screw3_name: Rear Right Screw +screw4: 38,175 +screw4_name: Rear Left Screw +horizontal_move_z: 5 +speed: 100 +screw_thread: CW-M4 + +[bed_screws] +screw1: 25,25 +screw1_name: Front Left Screw +screw2: 195,25 +screw2_name: Front Right Screw +screw3: 195,195 +screw3_name: Rear Right Screw +screw4: 25,195 +screw4_name: Rear Left Screw + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 5000 +max_z_velocity: 5 +max_z_accel: 100 +square_corner_velocity: 5.0 diff --git a/config/printer-lulzbot-mini1-2016.cfg b/config/printer-lulzbot-mini1-2016.cfg index 9be60cbdd..52b8061ee 100644 --- a/config/printer-lulzbot-mini1-2016.cfg +++ b/config/printer-lulzbot-mini1-2016.cfg @@ -125,7 +125,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.300 +value: 1.300 [output_pin stepper_z_current] pin: PL4 @@ -133,7 +133,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.630 +value: 1.630 [output_pin stepper_e_current] pin: PL5 @@ -141,7 +141,7 @@ pwm: True scale: 2.0 cycle_time: .000030 hardware_pwm: True -static_value: 1.250 +value: 1.250 [static_digital_output stepper_config] # Microstepping pins diff --git a/config/printer-wanhao-duplicator-6-2016.cfg b/config/printer-wanhao-duplicator-6-2016.cfg index b1d35faec..de8a3de87 100644 --- a/config/printer-wanhao-duplicator-6-2016.cfg +++ b/config/printer-wanhao-duplicator-6-2016.cfg @@ -86,7 +86,7 @@ pwm: True scale: 2.782 cycle_time: .000030 hardware_pwm: True -static_value: 1.2 +value: 1.2 [output_pin stepper_z_current] pin: PL4 @@ -94,7 +94,7 @@ pwm: True scale: 2.782 cycle_time: .000030 hardware_pwm: True -static_value: 1.2 +value: 1.2 [output_pin stepper_e_current] pin: PL3 @@ -102,7 +102,7 @@ pwm: True scale: 2.782 cycle_time: .000030 hardware_pwm: True -static_value: 1.0 +value: 1.0 [display] lcd_type: ssd1306 diff --git a/config/sample-macros.cfg b/config/sample-macros.cfg index 5132e1c99..f5649d61a 100644 --- a/config/sample-macros.cfg +++ b/config/sample-macros.cfg @@ -61,12 +61,10 @@ gcode: # P is the tone duration, S the tone frequency. # The frequency won't be pitch perfect. -[output_pin BEEPER_pin] +[pwm_cycle_time BEEPER_pin] pin: ar37 # Beeper pin. This parameter must be provided. # ar37 is the default RAMPS/MKS pin. -pwm: True -# A piezo beeper needs a PWM signal, a DC buzzer doesn't. value: 0 # Silent at power on, set to 1 if active low. shutdown_value: 0 diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index e759f961d..ada3de29d 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -142,7 +142,7 @@ bicubic_tension: 0.2 integer pair, and also may be specified a single integer that is applied to both axes. In this example there are 4 segments along the X axis and 2 segments along the Y axis. This evaluates to 8 interpolated - points along X, 6 interpolated points along Y, which results in a 13x8 + points along X, 6 interpolated points along Y, which results in a 13x9 mesh. Note that if mesh_pps is set to 0 then mesh interpolation is disabled and the probed matrix will be sampled directly. @@ -370,14 +370,68 @@ are identified in green. ![bedmesh_interpolated](img/bedmesh_faulty_regions.svg) +### Adaptive Meshes + +Adaptive bed meshing is a way to speed up the bed mesh generation by only probing +the area of the bed used by the objects being printed. When used, the method will +automatically adjust the mesh parameters based on the area occupied by the defined +print objects. + +The adapted mesh area will be computed from the area defined by the boundaries of all +the defined print objects so it covers every object, including any margins defined in +the configuration. After the area is computed, the number of probe points will be +scaled down based on the ratio of the default mesh area and the adapted mesh area. To +illustrate this consider the following example: + +For a 150mmx150mm bed with `mesh_min` set to `25,25` and `mesh_max` set to `125,125`, +the default mesh area is a 100mmx100mm square. An adapted mesh area of `50,50` +means a ratio of `0.5x0.5` between the adapted area and default mesh area. + +If the `bed_mesh` configuration specified `probe_count` as `7x7`, the adapted bed +mesh will use 4x4 probe points (7 * 0.5 rounded up). + +![adaptive_bedmesh](img/adaptive_bed_mesh.svg) + +``` +[bed_mesh] +speed: 120 +horizontal_move_z: 5 +mesh_min: 35, 6 +mesh_max: 240, 198 +probe_count: 5, 3 +adaptive_margin: 5 +``` + +- `adaptive_margin` \ + _Default Value: 0_ \ + Margin (in mm) to add around the area of the bed used by the defined objects. The diagram + below shows the adapted bed mesh area with an `adaptive_margin` of 5mm. The adapted mesh + area (area in green) is computed as the used bed area (area in blue) plus the defined margin. + + ![adaptive_bedmesh_margin](img/adaptive_bed_mesh_margin.svg) + +By nature, adaptive bed meshes use the objects defined by the Gcode file being printed. +Therefore, it is expected that each Gcode file will generate a mesh that probes a different +area of the print bed. Therefore, adapted bed meshes should not be re-used. The expectation +is that a new mesh will be generated for each print if adaptive meshing is used. + +It is also important to consider that adaptive bed meshing is best used on machines that can +normally probe the entire bed and achieve a maximum variance less than or equal to 1 layer +height. Machines with mechanical issues that a full bed mesh normally compensates for may +have undesirable results when attempting print moves **outside** of the probed area. If a +full bed mesh has a variance greater than 1 layer height, caution must be taken when using +adaptive bed meshes and attempting print moves outside of the meshed area. + ## Bed Mesh Gcodes ### Calibration `BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic] [=] - [=]`\ + [=] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=]`\ _Default Profile: default_\ -_Default Method: automatic if a probe is detected, otherwise manual_ +_Default Method: automatic if a probe is detected, otherwise manual_ \ +_Default Adaptive: 0_ \ +_Default Adaptive Margin: 0_ Initiates the probing procedure for Bed Mesh Calibration. @@ -399,6 +453,8 @@ following parameters are available: - `ROUND_PROBE_COUNT` - All beds: - `ALGORITHM` + - `ADAPTIVE` + - `ADAPTIVE_MARGIN` See the configuration documentation above for details on how each parameter applies to the mesh. diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index 887793561..a1c7960ad 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -136,8 +136,9 @@ provides further information on the mechanics of moves. * The ToolHead class (in toolhead.py) handles "look-ahead" and tracks the timing of printing actions. The main codepath for a move is: - `ToolHead.move() -> MoveQueue.add_move() -> MoveQueue.flush() -> - Move.set_junction() -> ToolHead._process_moves()`. + `ToolHead.move() -> LookAheadQueue.add_move() -> + LookAheadQueue.flush() -> Move.set_junction() -> + ToolHead._process_moves()`. * ToolHead.move() creates a Move() object with the parameters of the move (in cartesian space and in units of seconds and millimeters). * The kinematics class is given the opportunity to audit each move @@ -146,10 +147,10 @@ provides further information on the mechanics of moves. may raise an error if the move is not valid. If check_move() completes successfully then the underlying kinematics must be able to handle the move. - * MoveQueue.add_move() places the move object on the "look-ahead" - queue. - * MoveQueue.flush() determines the start and end velocities of each - move. + * LookAheadQueue.add_move() places the move object on the + "look-ahead" queue. + * LookAheadQueue.flush() determines the start and end velocities of + each move. * Move.set_junction() implements the "trapezoid generator" on a move. The "trapezoid generator" breaks every move into three parts: a constant acceleration phase, followed by a constant velocity @@ -170,17 +171,18 @@ provides further information on the mechanics of moves. placed on a "trapezoid motion queue": `ToolHead._process_moves() -> trapq_append()` (in klippy/chelper/trapq.c). The step times are then generated: `ToolHead._process_moves() -> - ToolHead._update_move_time() -> MCU_Stepper.generate_steps() -> - itersolve_generate_steps() -> itersolve_gen_steps_range()` (in - klippy/chelper/itersolve.c). The goal of the iterative solver is to - find step times given a function that calculates a stepper position - from a time. This is done by repeatedly "guessing" various times - until the stepper position formula returns the desired position of - the next step on the stepper. The feedback produced from each guess - is used to improve future guesses so that the process rapidly - converges to the desired time. The kinematic stepper position - formulas are located in the klippy/chelper/ directory (eg, - kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c). + ToolHead._advance_move_time() -> ToolHead._advance_flush_time() -> + MCU_Stepper.generate_steps() -> itersolve_generate_steps() -> + itersolve_gen_steps_range()` (in klippy/chelper/itersolve.c). The + goal of the iterative solver is to find step times given a function + that calculates a stepper position from a time. This is done by + repeatedly "guessing" various times until the stepper position + formula returns the desired position of the next step on the + stepper. The feedback produced from each guess is used to improve + future guesses so that the process rapidly converges to the desired + time. The kinematic stepper position formulas are located in the + klippy/chelper/ directory (eg, kin_cart.c, kin_corexy.c, + kin_delta.c, kin_extruder.c). * Note that the extruder is handled in its own kinematic class: `ToolHead._process_moves() -> PrinterExtruder.move()`. Since diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 2768c811f..7954d427c 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,19 @@ All dates in this document are approximate. ## Changes +20240123: The output_pin SET_PIN CYCLE_TIME parameter has been +removed. Use the new +[pwm_cycle_time](Config_Reference.md#pwm_cycle_time) module if it is +necessary to dynamically change a pwm pin's cycle time. + +20240123: The output_pin `maximum_mcu_duration` parameter is +deprecated. Use a [pwm_tool config section](Config_Reference.md#pwm_tool) +instead. The option will be removed in the near future. + +20240123: The output_pin `static_value` parameter is deprecated. +Replace with `value` and `shutdown_value` parameters. The option will +be removed in the near future. + 20231216: The `[hall_filament_width_sensor]` is changed to trigger filament runout when the thickness of the filament exceeds `max_diameter`. The maximum diameter defaults to `default_nominal_filament_diameter + max_difference`. See diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 29c159d22..a3c2490f7 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -110,6 +110,11 @@ A collection of DangerKlipper-specific system options # Tolerance (in mm) for distance moved in the second homing. Ensures the # second homing distance closely matches the `min_home_dist` when using # sensorless homing. The default is 0.5mm. +#adc_ignore_limits: False +# When set to true, this parameter ignores the min_value and max_value +# limits for ADC temperature sensors. It prevents shutdowns due to +# 'ADC out of range' errors by allowing readings outside the specified +# range without triggering a shutdown. Default is False. ``` ## Common kinematic settings @@ -1041,6 +1046,9 @@ Visual Examples: # Optional points that define a faulty region. See docs/Bed_Mesh.md # for details on faulty regions. Up to 99 faulty regions may be added. # By default no faulty regions are set. +#adaptive_margin: +# An optional margin (in mm) to be added around the bed area used by +# the defined print objects when generating an adaptive mesh. ``` ### [bed_tilt] @@ -3385,24 +3393,12 @@ pin: # If this is true, the value fields should be between 0 and 1; if it # is false the value fields should be either 0 or 1. The default is # False. -#static_value: -# If this is set, then the pin is assigned to this value at startup -# and the pin can not be changed during runtime. A static pin uses -# slightly less ram in the micro-controller. The default is to use -# runtime configuration of pins. #value: # The value to initially set the pin to during MCU configuration. # The default is 0 (for low voltage). #shutdown_value: # The value to set the pin to on an MCU shutdown event. The default # is 0 (for low voltage). -#maximum_mcu_duration: -# The maximum duration a non-shutdown value may be driven by the MCU -# without an acknowledge from the host. -# If host can not keep up with an update, the MCU will shutdown -# and set all pins to their respective shutdown values. -# Default: 0 (disabled) -# Usual values are around 5 seconds. #cycle_time: 0.100 # The amount of time (in seconds) per PWM cycle. It is recommended # this be 10 milliseconds or greater when using software based PWM. @@ -3422,6 +3418,9 @@ pin: # then the 'value' parameter can be specified using the desired # amperage for the stepper. The default is to not scale the 'value' # parameter. +#maximum_mcu_duration: +#static_value: +# These options are deprecated and should no longer be specified. ``` ### [pwm_tool] @@ -3436,15 +3435,39 @@ extended [g-code commands](G-Codes.md#output_pin). [pwm_tool my_tool] pin: # The pin to configure as an output. This parameter must be provided. +#maximum_mcu_duration: +# The maximum duration a non-shutdown value may be driven by the MCU +# without an acknowledge from the host. +# If host can not keep up with an update, the MCU will shutdown +# and set all pins to their respective shutdown values. +# Default: 0 (disabled) +# Usual values are around 5 seconds. #value: #shutdown_value: -#maximum_mcu_duration: #cycle_time: 0.100 #hardware_pwm: False #scale: # See the "output_pin" section for the definition of these parameters. ``` +### [pwm_cycle_time] + +Run-time configurable output pins with dynamic pwm cycle timing (one +may define any number of sections with an "pwm_cycle_time" prefix). +Pins configured here will be setup as output pins and one may modify +them at run-time using "SET_PIN PIN=my_pin VALUE=.1 CYCLE_TIME=0.100" +type extended [g-code commands](G-Codes.md#pwm_cycle_time). + +``` +[pwm_cycle_time my_pin] +pin: +#value: +#shutdown_value: +#cycle_time: 0.100 +#scale: +# See the "output_pin" section for information on these parameters. +``` + ### [static_digital_output] Statically configured digital output pins (one may define any number diff --git a/docs/G-Codes.md b/docs/G-Codes.md index eb64894dc..02081e7ea 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -88,12 +88,19 @@ accelerometer does not have a name in its config section (simply `[adxl345]`) then `` part of the name is not generated. #### ACCELEROMETER_QUERY -`ACCELEROMETER_QUERY [CHIP=] [RATE=]`: queries +`ACCELEROMETER_QUERY [CHIP=] [RATE=] +[SAMPLES=] [RETURN=]`: queries accelerometer for the current value. If CHIP is not specified it defaults to "adxl345". If RATE is not specified, the default value is used. This command is useful to test the connection to the ADXL345 accelerometer: one of the returned values should be a free-fall -acceleration (+/- some noise of the chip). +acceleration (+/- some noise of the chip). The `SAMPLES` parameter +can be set to sample multiple readings from the sensor. The readings +will be averaged together. The default is to collect a single sample. +The `RETURN` parameter can take on the values `vector`(the default) or +`tilt`. In `vector` mode, the raw free-fall acceleration vector is +returned. In `tilt` mode, X and Y angles of a plane perpendicular to +the free-fall vector are calculated and displayed. #### ACCELEROMETER_DEBUG_READ `ACCELEROMETER_DEBUG_READ [CHIP=] REG=`: @@ -898,17 +905,10 @@ The following command is available when an enabled. #### SET_PIN -`SET_PIN PIN=config_name VALUE= [CYCLE_TIME=]`: Set -the pin to the given output `VALUE`. VALUE should be 0 or 1 for -"digital" output pins. For PWM pins, set to a value between 0.0 and -1.0, or between 0.0 and `scale` if a scale is configured in the -output_pin config section. - -Some pins (currently only "soft PWM" pins) support setting an explicit -cycle time using the CYCLE_TIME parameter (specified in seconds). Note -that the CYCLE_TIME parameter is not stored between SET_PIN commands -(any SET_PIN command without an explicit CYCLE_TIME parameter will use -the `cycle_time` specified in the output_pin config section). +`SET_PIN PIN=config_name VALUE=`: Set the pin to the given +output `VALUE`. VALUE should be 0 or 1 for "digital" output pins. For +PWM pins, set to a value between 0.0 and 1.0, or between 0.0 and +`scale` if a scale is configured in the output_pin config section. ### [palette2] @@ -1046,6 +1046,21 @@ babystepping), and subtract if from the probe's z_offset. This acts to take a frequently used babystepping value, and "make it permanent". Requires a `SAVE_CONFIG` to take effect. +### [pwm_cycle_time] + +The following command is available when a +[pwm_cycle_time config section](Config_Reference.md#pwm_cycle_time) +is enabled. + +#### SET_PIN +`SET_PIN PIN=config_name VALUE= [CYCLE_TIME=]`: +This command works similarly to [output_pin](#output_pin) SET_PIN +commands. The command here supports setting an explicit cycle time +using the CYCLE_TIME parameter (specified in seconds). Note that the +CYCLE_TIME parameter is not stored between SET_PIN commands (any +SET_PIN command without an explicit CYCLE_TIME parameter will use the +`cycle_time` specified in the pwm_cycle_time config section). + ### [query_adc] The query_adc module is automatically loaded. diff --git a/docs/Multi_MCU_Homing.md b/docs/Multi_MCU_Homing.md index 22d2508e4..c32a5947c 100644 --- a/docs/Multi_MCU_Homing.md +++ b/docs/Multi_MCU_Homing.md @@ -31,9 +31,15 @@ overshoot and account for it in its calculations. However, it is important that the hardware design is capable of handling overshoot without causing damage to the machine. -Should Klipper detect a communication issue between micro-controllers -during multi-mcu homing then it will raise a "Communication timeout -during homing" error. +In order to use this "multi-mcu homing" capability the hardware must +have predictably low latency between the host computer and all of the +micro-controllers. Typically the round-trip time must be consistently +less than 10ms. High latency (even for short periods) is likely to +result in homing failures. + +Should high latency result in a failure (or if some other +communication issue is detected) then Klipper will raise a +"Communication timeout during homing" error. Note that an axis with multiple steppers (eg, `stepper_z` and `stepper_z1`) need to be on the same micro-controller in order to use diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 52f16467b..8f8c74df0 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -394,6 +394,13 @@ is defined): template expansion, the PROBE (or similar) command must be run prior to the macro containing this reference. +## pwm_cycle_time + +The following information is available in +[pwm_cycle_time some_name](Config_Reference.md#pwm_cycle_time) +objects: +- `value`: The "value" of the pin, as set by a `SET_PIN` command. + ## quad_gantry_level The following information is available in the `quad_gantry_level` object diff --git a/docs/_klipper3d/mkdocs-requirements.txt b/docs/_klipper3d/mkdocs-requirements.txt index 9ea6d2192..739288959 100644 --- a/docs/_klipper3d/mkdocs-requirements.txt +++ b/docs/_klipper3d/mkdocs-requirements.txt @@ -1,6 +1,6 @@ # Python virtualenv module requirements for mkdocs -jinja2==3.0.3 -mkdocs==1.2.3 +jinja2==3.1.3 +mkdocs==1.2.4 mkdocs-material==8.1.3 mkdocs-simple-hooks==0.1.3 mkdocs-exclude==1.0.2 diff --git a/docs/img/adaptive_bed_mesh.svg b/docs/img/adaptive_bed_mesh.svg new file mode 100644 index 000000000..954ca0b32 --- /dev/null +++ b/docs/img/adaptive_bed_mesh.svg @@ -0,0 +1,4 @@ + + + +
Origin
(0,0)
Origin...

Legend


Legend
Object Polygon
Object Polygon
Used Bed Area
Used Bed Area
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/img/adaptive_bed_mesh_margin.svg b/docs/img/adaptive_bed_mesh_margin.svg new file mode 100644 index 000000000..6c6216d04 --- /dev/null +++ b/docs/img/adaptive_bed_mesh_margin.svg @@ -0,0 +1,4 @@ + + + +
Origin
(0,0)
Origin...

Legend


Legend
Object Polygon
Object Polygon
Used Bed Area
Used Bed Area
Adapted Bed Mesh Area
Adapted Bed Mesh Area
(90,75)
(90,75)
(130,140)
(130,140)
(125,135)
(125,1...
(95,120)
(95,12...
(60,90)
(60,90)
(90,130)
(90,13...
(95,80)
(95,80)
(125,110)
(125,1...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/klippy/chelper/__init__.py b/klippy/chelper/__init__.py index 608e62d1a..f8cf2e588 100644 --- a/klippy/chelper/__init__.py +++ b/klippy/chelper/__init__.py @@ -82,7 +82,8 @@ void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss , double time_offset, double mcu_freq); - int steppersync_flush(struct steppersync *ss, uint64_t move_clock); + int steppersync_flush(struct steppersync *ss, uint64_t move_clock + , uint64_t clear_history_clock); """ defs_itersolve = """ @@ -116,7 +117,8 @@ , double start_pos_x, double start_pos_y, double start_pos_z , double axes_r_x, double axes_r_y, double axes_r_z , double start_v, double cruise_v, double accel); - void trapq_finalize_moves(struct trapq *tq, double print_time); + void trapq_finalize_moves(struct trapq *tq, double print_time + , double clear_history_time); void trapq_set_position(struct trapq *tq, double print_time , double pos_x, double pos_y, double pos_z); int trapq_extract_old(struct trapq *tq, struct pull_move *p, int max @@ -258,6 +260,7 @@ defs_kin_idex, ] + # Update filenames to an absolute path def get_abs_files(srcdir, filelist): return [os.path.join(srcdir, fname) for fname in filelist] @@ -305,6 +308,7 @@ def do_build_code(cmd): FFI_lib = None pyhelper_logging_callback = None + # Hepler invoked from C errorf() code to log errors def logging_callback(msg): logging.error(FFI_main.string(msg)) diff --git a/klippy/chelper/stepcompress.c b/klippy/chelper/stepcompress.c index e5514b952..310f2bf31 100644 --- a/klippy/chelper/stepcompress.c +++ b/klippy/chelper/stepcompress.c @@ -54,8 +54,6 @@ struct step_move { int16_t add; }; -#define HISTORY_EXPIRE (30.0) - struct history_steps { struct list_node node; uint64_t first_clock, last_clock; @@ -292,6 +290,13 @@ free_history(struct stepcompress *sc, uint64_t end_clock) } } +// Expire the stepcompress history older than the given clock +static void +stepcompress_history_expire(struct stepcompress *sc, uint64_t end_clock) +{ + free_history(sc, end_clock); +} + // Free memory associated with a 'stepcompress' object void __visible stepcompress_free(struct stepcompress *sc) @@ -322,9 +327,6 @@ calc_last_step_print_time(struct stepcompress *sc) { double lsc = sc->last_step_clock; sc->last_step_print_time = sc->mcu_time_offset + (lsc - .5) / sc->mcu_freq; - - if (lsc > sc->mcu_freq * HISTORY_EXPIRE) - free_history(sc, lsc - sc->mcu_freq * HISTORY_EXPIRE); } // Set the conversion rate of 'print_time' to mcu clock @@ -731,6 +733,18 @@ steppersync_set_time(struct steppersync *ss, double time_offset } } +// Expire the stepcompress history before the given clock time +static void +steppersync_history_expire(struct steppersync *ss, uint64_t end_clock) +{ + int i; + for (i = 0; i < ss->sc_num; i++) + { + struct stepcompress *sc = ss->sc_list[i]; + stepcompress_history_expire(sc, end_clock); + } +} + // Implement a binary heap algorithm to track when the next available // 'struct move' in the mcu will be available static void @@ -758,7 +772,8 @@ heap_replace(struct steppersync *ss, uint64_t req_clock) // Find and transmit any scheduled steps prior to the given 'move_clock' int __visible -steppersync_flush(struct steppersync *ss, uint64_t move_clock) +steppersync_flush(struct steppersync *ss, uint64_t move_clock + , uint64_t clear_history_clock) { // Flush each stepcompress to the specified move_clock int i; @@ -806,5 +821,7 @@ steppersync_flush(struct steppersync *ss, uint64_t move_clock) // Transmit commands if (!list_empty(&msgs)) serialqueue_send_batch(ss->sq, ss->cq, &msgs); + + steppersync_history_expire(ss, clear_history_clock); return 0; } diff --git a/klippy/chelper/stepcompress.h b/klippy/chelper/stepcompress.h index bfc0dfcde..c5b40383f 100644 --- a/klippy/chelper/stepcompress.h +++ b/klippy/chelper/stepcompress.h @@ -42,6 +42,7 @@ struct steppersync *steppersync_alloc( void steppersync_free(struct steppersync *ss); void steppersync_set_time(struct steppersync *ss, double time_offset , double mcu_freq); -int steppersync_flush(struct steppersync *ss, uint64_t move_clock); +int steppersync_flush(struct steppersync *ss, uint64_t move_clock + , uint64_t clear_history_clock); #endif // stepcompress.h diff --git a/klippy/chelper/trapq.c b/klippy/chelper/trapq.c index 9b1b501b4..b9930e997 100644 --- a/klippy/chelper/trapq.c +++ b/klippy/chelper/trapq.c @@ -163,11 +163,10 @@ trapq_append(struct trapq *tq, double print_time } } -#define HISTORY_EXPIRE (30.0) - // Expire any moves older than `print_time` from the trapezoid velocity queue void __visible -trapq_finalize_moves(struct trapq *tq, double print_time) +trapq_finalize_moves(struct trapq *tq, double print_time + , double clear_history_time) { struct move *head_sentinel = list_first_entry(&tq->moves, struct move,node); struct move *tail_sentinel = list_last_entry(&tq->moves, struct move, node); @@ -190,10 +189,9 @@ trapq_finalize_moves(struct trapq *tq, double print_time) if (list_empty(&tq->history)) return; struct move *latest = list_first_entry(&tq->history, struct move, node); - double expire_time = latest->print_time + latest->move_t - HISTORY_EXPIRE; for (;;) { struct move *m = list_last_entry(&tq->history, struct move, node); - if (m == latest || m->print_time + m->move_t > expire_time) + if (m == latest || m->print_time + m->move_t > clear_history_time) break; list_del(&m->node); free(m); @@ -206,7 +204,7 @@ trapq_set_position(struct trapq *tq, double print_time , double pos_x, double pos_y, double pos_z) { // Flush all moves from trapq - trapq_finalize_moves(tq, NEVER_TIME); + trapq_finalize_moves(tq, NEVER_TIME, 0); // Prune any moves in the trapq history that were interrupted while (!list_empty(&tq->history)) { diff --git a/klippy/chelper/trapq.h b/klippy/chelper/trapq.h index bd8f4e8c2..c463f0c53 100644 --- a/klippy/chelper/trapq.h +++ b/klippy/chelper/trapq.h @@ -43,7 +43,8 @@ void trapq_append(struct trapq *tq, double print_time , double start_pos_x, double start_pos_y, double start_pos_z , double axes_r_x, double axes_r_y, double axes_r_z , double start_v, double cruise_v, double accel); -void trapq_finalize_moves(struct trapq *tq, double print_time); +void trapq_finalize_moves(struct trapq *tq, double print_time + , double clear_history_time); void trapq_set_position(struct trapq *tq, double print_time , double pos_x, double pos_y, double pos_z); int trapq_extract_old(struct trapq *tq, struct pull_move *p, int max diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index 29faf3097..9164f78bd 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -15,6 +15,7 @@ REPORT_TIME = 0.300 RANGE_CHECK_COUNT = 4 + # Interface between ADC and heater temperature callbacks class PrinterADCtoTemperature: def __init__(self, config, adc_convert): @@ -24,6 +25,9 @@ def __init__(self, config, adc_convert): self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) query_adc = config.get_printer().load_object(config, "query_adc") query_adc.register_adc(config.get_name(), self.mcu_adc) + self.danger_options = config.get_printer().lookup_object( + "danger_options" + ) def setup_callback(self, temperature_callback): self.temperature_callback = temperature_callback @@ -36,13 +40,19 @@ def adc_callback(self, read_time, read_value): self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp) def setup_minmax(self, min_temp, max_temp): + if self.danger_options.adc_ignore_limits: + danger_check_count = 0 + else: + danger_check_count = RANGE_CHECK_COUNT + adc_range = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]] + self.mcu_adc.setup_minmax( SAMPLE_TIME, SAMPLE_COUNT, minval=min(adc_range), maxval=max(adc_range), - range_check_count=RANGE_CHECK_COUNT, + range_check_count=danger_check_count, ) @@ -50,6 +60,7 @@ def setup_minmax(self, min_temp, max_temp): # Linear interpolation ###################################################################### + # Helper code to perform linear interpolation class LinearInterpolate: def __init__(self, samples): @@ -98,6 +109,7 @@ def reverse_interpolate(self, value): # Linear voltage to temperature converter ###################################################################### + # Linear style conversion chips calibrated from temperature measurements class LinearVoltage: def __init__(self, config, params): @@ -146,6 +158,7 @@ def create(self, config): # Linear resistance to temperature converter ###################################################################### + # Linear resistance calibrated from temperature measurements class LinearResistance: def __init__(self, config, samples): diff --git a/klippy/extras/adxl345.py b/klippy/extras/adxl345.py index 03e4d8b4b..7de45c570 100644 --- a/klippy/extras/adxl345.py +++ b/klippy/extras/adxl345.py @@ -1,10 +1,10 @@ # Support for reading acceleration data from an adxl345 chip # -# Copyright (C) 2020-2021 Kevin O'Connor +# Copyright (C) 2020-2023 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, time, collections, threading, multiprocessing, os -from . import bus, motion_report +import logging, time, collections, multiprocessing, os, math +from . import bus, bulk_sensor # ADXL345 registers REG_DEVID = 0x00 @@ -37,31 +37,35 @@ "Accel_Measurement", ("time", "accel_x", "accel_y", "accel_z") ) + # Helper class to obtain measurements class AccelQueryHelper: - def __init__(self, printer, cconn): + def __init__(self, printer): self.printer = printer - self.cconn = cconn + self.is_finished = False print_time = printer.lookup_object("toolhead").get_last_move_time() self.request_start_time = self.request_end_time = print_time - self.samples = self.raw_samples = [] + self.msgs = [] + self.samples = [] def finish_measurements(self): toolhead = self.printer.lookup_object("toolhead") self.request_end_time = toolhead.get_last_move_time() toolhead.wait_moves() - self.cconn.finalize() + self.is_finished = True - def _get_raw_samples(self): - raw_samples = self.cconn.get_messages() - if raw_samples: - self.raw_samples = raw_samples - return self.raw_samples + def handle_batch(self, msg): + if self.is_finished: + return False + if len(self.msgs) >= 10000: + # Avoid filling up memory with too many samples + return False + self.msgs.append(msg) + return True def has_valid_samples(self): - raw_samples = self._get_raw_samples() - for msg in raw_samples: - data = msg["params"]["data"] + for msg in self.msgs: + data = msg["data"] first_sample_time = data[0][0] last_sample_time = data[-1][0] if ( @@ -72,7 +76,7 @@ def has_valid_samples(self): # The time intervals [first_sample_time, last_sample_time] # and [request_start_time, request_end_time] have non-zero # intersection. It is still theoretically possible that none - # of the samples from raw_samples fall into the time interval + # of the samples from msgs fall into the time interval # [request_start_time, request_end_time] if it is too narrow # or on very heavy data losses. In practice, that interval # is at least 1 second, so this possibility is negligible. @@ -80,14 +84,13 @@ def has_valid_samples(self): return False def get_samples(self): - raw_samples = self._get_raw_samples() - if not raw_samples: + if not self.msgs: return self.samples - total = sum([len(m["params"]["data"]) for m in raw_samples]) + total = sum([len(m["data"]) for m in self.msgs]) count = 0 self.samples = samples = [None] * total - for msg in raw_samples: - for samp_time, x, y, z in msg["params"]["data"]: + for msg in self.msgs: + for samp_time, x, y, z in msg["data"]: if samp_time < self.request_start_time: continue if samp_time > self.request_end_time: @@ -192,17 +195,38 @@ def cmd_ACCELEROMETER_MEASURE(self, gcmd): cmd_ACCELEROMETER_QUERY_help = "Query accelerometer for the current values" def cmd_ACCELEROMETER_QUERY(self, gcmd): - aclient = self.chip.start_internal_client() - self.printer.lookup_object("toolhead").dwell(1.0) - aclient.finish_measurements() - values = aclient.get_samples() - if not values: - raise gcmd.error("No accelerometer measurements found") - _, accel_x, accel_y, accel_z = values[-1] - gcmd.respond_info( - "accelerometer values (x, y, z): %.6f, %.6f, %.6f" - % (accel_x, accel_y, accel_z) - ) + num_samples = gcmd.get_int("SAMPLES", 1) + samples = [] + while num_samples > 0: + aclient = self.chip.start_internal_client() + self.printer.lookup_object("toolhead").dwell(1.0) + aclient.finish_measurements() + values = aclient.get_samples() + if not values: + raise gcmd.error("No accelerometer measurements found") + take = min(len(values), num_samples) + num_samples -= take + samples.extend(values[-take:]) + + accel_x = sum([x for (_, x, y, z) in samples]) / len(samples) + accel_y = sum([y for (_, x, y, z) in samples]) / len(samples) + accel_z = sum([z for (_, x, y, z) in samples]) / len(samples) + + return_type = gcmd.get("RETURN", "vector") + if return_type == "tilt": + tilt_x = math.degrees(math.atan2(accel_x, accel_z)) + tilt_y = math.degrees(math.atan2(accel_y, accel_z)) + gcmd.respond_info( + "accelerometer plane tilt degrees (x, y): %.6f, %.6f" + % (tilt_x, tilt_y) + ) + elif return_type == "vector": + gcmd.respond_info( + "accelerometer values (x, y, z): %.6f, %.6f, %.6f" + % (accel_x, accel_y, accel_z) + ) + else: + raise gcmd.error("Unknown 'return' type '%s'" % (return_type,)) cmd_ACCELEROMETER_DEBUG_READ_help = "Query register (for debugging)" @@ -219,144 +243,78 @@ def cmd_ACCELEROMETER_DEBUG_WRITE(self, gcmd): self.chip.set_reg(reg, val) -# Helper class for chip clock synchronization via linear regression -class ClockSyncRegression: - def __init__(self, mcu, chip_clock_smooth, decay=1.0 / 20.0): - self.mcu = mcu - self.chip_clock_smooth = chip_clock_smooth - self.decay = decay - self.last_chip_clock = self.last_exp_mcu_clock = 0.0 - self.mcu_clock_avg = self.mcu_clock_variance = 0.0 - self.chip_clock_avg = self.chip_clock_covariance = 0.0 - - def reset(self, mcu_clock, chip_clock): - self.mcu_clock_avg = self.last_mcu_clock = mcu_clock - self.chip_clock_avg = chip_clock - self.mcu_clock_variance = self.chip_clock_covariance = 0.0 - self.last_chip_clock = self.last_exp_mcu_clock = 0.0 - - def update(self, mcu_clock, chip_clock): - # Update linear regression - decay = self.decay - diff_mcu_clock = mcu_clock - self.mcu_clock_avg - self.mcu_clock_avg += decay * diff_mcu_clock - self.mcu_clock_variance = (1.0 - decay) * ( - self.mcu_clock_variance + diff_mcu_clock**2 * decay - ) - diff_chip_clock = chip_clock - self.chip_clock_avg - self.chip_clock_avg += decay * diff_chip_clock - self.chip_clock_covariance = (1.0 - decay) * ( - self.chip_clock_covariance - + diff_mcu_clock * diff_chip_clock * decay - ) - - def set_last_chip_clock(self, chip_clock): - base_mcu, base_chip, inv_cfreq = self.get_clock_translation() - self.last_chip_clock = chip_clock - self.last_exp_mcu_clock = ( - base_mcu + (chip_clock - base_chip) * inv_cfreq - ) +# Helper to read the axes_map parameter from the config +def read_axes_map(config): + am = { + "x": (0, SCALE_XY), + "y": (1, SCALE_XY), + "z": (2, SCALE_Z), + "-x": (0, -SCALE_XY), + "-y": (1, -SCALE_XY), + "-z": (2, -SCALE_Z), + } + axes_map = config.getlist("axes_map", ("x", "y", "z"), count=3) + if any([a not in am for a in axes_map]): + raise config.error("Invalid axes_map parameter") + return [am[a.strip()] for a in axes_map] - def get_clock_translation(self): - inv_chip_freq = self.mcu_clock_variance / self.chip_clock_covariance - if not self.last_chip_clock: - return self.mcu_clock_avg, self.chip_clock_avg, inv_chip_freq - # Find mcu clock associated with future chip_clock - s_chip_clock = self.last_chip_clock + self.chip_clock_smooth - scdiff = s_chip_clock - self.chip_clock_avg - s_mcu_clock = self.mcu_clock_avg + scdiff * inv_chip_freq - # Calculate frequency to converge at future point - mdiff = s_mcu_clock - self.last_exp_mcu_clock - s_inv_chip_freq = mdiff / self.chip_clock_smooth - return self.last_exp_mcu_clock, self.last_chip_clock, s_inv_chip_freq - - def get_time_translation(self): - base_mcu, base_chip, inv_cfreq = self.get_clock_translation() - clock_to_print_time = self.mcu.clock_to_print_time - base_time = clock_to_print_time(base_mcu) - inv_freq = clock_to_print_time(base_mcu + inv_cfreq) - base_time - return base_time, base_chip, inv_freq - - -MIN_MSG_TIME = 0.100 BYTES_PER_SAMPLE = 5 -SAMPLES_PER_BLOCK = 10 +SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE + +BATCH_UPDATES = 0.100 + # Printer class that controls ADXL345 chip class ADXL345: def __init__(self, config): self.printer = config.get_printer() AccelCommandHelper(config, self) - self.query_rate = 0 - am = { - "x": (0, SCALE_XY), - "y": (1, SCALE_XY), - "z": (2, SCALE_Z), - "-x": (0, -SCALE_XY), - "-y": (1, -SCALE_XY), - "-z": (2, -SCALE_Z), - } - axes_map = config.getlist("axes_map", ("x", "y", "z"), count=3) - if any([a not in am for a in axes_map]): - raise config.error("Invalid adxl345 axes_map parameter") - self.axes_map = [am[a.strip()] for a in axes_map] + self.axes_map = read_axes_map(config) self.data_rate = config.getint("rate", 3200) if self.data_rate not in QUERY_RATES: raise config.error("Invalid rate parameter: %d" % (self.data_rate,)) - # Measurement storage (accessed from background thread) - self.lock = threading.Lock() - self.raw_samples = [] # Setup mcu sensor_adxl345 bulk query code self.spi = bus.MCU_SPI_from_config(config, 3, default_speed=5000000) self.mcu = mcu = self.spi.get_mcu() self.oid = oid = mcu.create_oid() - self.query_adxl345_cmd = self.query_adxl345_end_cmd = None - self.query_adxl345_status_cmd = None + self.query_adxl345_cmd = None mcu.add_config_cmd( "config_adxl345 oid=%d spi_oid=%d" % (oid, self.spi.get_oid()) ) mcu.add_config_cmd( - "query_adxl345 oid=%d clock=0 rest_ticks=0" % (oid,), - on_restart=True, + "query_adxl345 oid=%d rest_ticks=0" % (oid,), on_restart=True ) mcu.register_config_callback(self._build_config) - mcu.register_response(self._handle_adxl345_data, "adxl345_data", oid) + self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=oid) # Clock tracking - self.last_sequence = self.max_query_duration = 0 - self.last_limit_count = self.last_error_count = 0 - self.clock_sync = ClockSyncRegression(self.mcu, 640) - # API server endpoints - self.api_dump = motion_report.APIDumpHelper( - self.printer, self._api_update, self._api_startstop, 0.100 + chip_smooth = self.data_rate * BATCH_UPDATES * 2 + self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth) + self.clock_updater = bulk_sensor.ChipClockUpdater( + self.clock_sync, BYTES_PER_SAMPLE + ) + self.last_error_count = 0 + # Process messages in batches + self.batch_bulk = bulk_sensor.BatchBulkHelper( + self.printer, + self._process_batch, + self._start_measurements, + self._finish_measurements, + BATCH_UPDATES, ) self.name = config.get_name().split()[-1] - wh = self.printer.lookup_object("webhooks") - wh.register_mux_endpoint( - "adxl345/dump_adxl345", - "sensor", - self.name, - self._handle_dump_adxl345, + hdr = ("time", "x_acceleration", "y_acceleration", "z_acceleration") + self.batch_bulk.add_mux_endpoint( + "adxl345/dump_adxl345", "sensor", self.name, {"header": hdr} ) def _build_config(self): cmdqueue = self.spi.get_command_queue() self.query_adxl345_cmd = self.mcu.lookup_command( - "query_adxl345 oid=%c clock=%u rest_ticks=%u", cq=cmdqueue + "query_adxl345 oid=%c rest_ticks=%u", cq=cmdqueue ) - self.query_adxl345_end_cmd = self.mcu.lookup_query_command( - "query_adxl345 oid=%c clock=%u rest_ticks=%u", - "adxl345_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%c limit_count=%hu", - oid=self.oid, - cq=cmdqueue, - ) - self.query_adxl345_status_cmd = self.mcu.lookup_query_command( - "query_adxl345_status oid=%c", - "adxl345_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%c limit_count=%hu", - oid=self.oid, - cq=cmdqueue, + self.clock_updater.setup_query_command( + self.mcu, "query_adxl345_status oid=%c", oid=self.oid, cq=cmdqueue ) def read_reg(self, reg): @@ -375,18 +333,16 @@ def set_reg(self, reg, val, minclock=0): % (reg, val, stored_val) ) - # Measurement collection - def is_measuring(self): - return self.query_rate > 0 - - def _handle_adxl345_data(self, params): - with self.lock: - self.raw_samples.append(params) + def start_internal_client(self): + aqh = AccelQueryHelper(self.printer) + self.batch_bulk.add_client(aqh.handle_batch) + return aqh + # Measurement decoding def _extract_samples(self, raw_samples): # Load variables to optimize inner loop below (x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map - last_sequence = self.last_sequence + last_sequence = self.clock_updater.get_last_sequence() time_base, chip_base, inv_freq = self.clock_sync.get_time_translation() # Process every message in raw_samples count = seq = 0 @@ -419,45 +375,8 @@ def _extract_samples(self, raw_samples): del samples[count:] return samples - def _update_clock(self, minclock=0): - # Query current state - for retry in range(5): - params = self.query_adxl345_status_cmd.send( - [self.oid], minclock=minclock - ) - fifo = params["fifo"] & 0x7F - if fifo <= 32: - break - else: - raise self.printer.command_error("Unable to query adxl345 fifo") - mcu_clock = self.mcu.clock32_to_clock64(params["clock"]) - seq_diff = (params["next_sequence"] - self.last_sequence) & 0xFFFF - self.last_sequence += seq_diff - buffered = params["buffered"] - lc_diff = (params["limit_count"] - self.last_limit_count) & 0xFFFF - self.last_limit_count += lc_diff - duration = params["query_ticks"] - if duration > self.max_query_duration: - # Skip measurement as a high query time could skew clock tracking - self.max_query_duration = max( - 2 * self.max_query_duration, self.mcu.seconds_to_clock(0.000005) - ) - return - self.max_query_duration = 2 * duration - msg_count = ( - self.last_sequence * SAMPLES_PER_BLOCK - + buffered // BYTES_PER_SAMPLE - + fifo - ) - # The "chip clock" is the message counter plus .5 for average - # inaccuracy of query responses and plus .5 for assumed offset - # of adxl345 hw processing time. - chip_clock = msg_count + 1 - self.clock_sync.update(mcu_clock + duration // 2, chip_clock) - + # Start, stop, and process message batches def _start_measurements(self): - if self.is_measuring(): - return # In case of miswiring, testing ADXL345 device ID prevents treating # noise or wrong signal as a correctly initialized device dev_id = self.read_reg(REG_DEVID) @@ -474,43 +393,26 @@ def _start_measurements(self): self.set_reg(REG_FIFO_CTL, 0x00) self.set_reg(REG_BW_RATE, QUERY_RATES[self.data_rate]) self.set_reg(REG_FIFO_CTL, SET_FIFO_CTL) - # Setup samples - with self.lock: - self.raw_samples = [] # Start bulk reading - systime = self.printer.get_reactor().monotonic() - print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME - reqclock = self.mcu.print_time_to_clock(print_time) + self.bulk_queue.clear_samples() rest_ticks = self.mcu.seconds_to_clock(4.0 / self.data_rate) - self.query_rate = self.data_rate - self.query_adxl345_cmd.send( - [self.oid, reqclock, rest_ticks], reqclock=reqclock - ) + self.query_adxl345_cmd.send([self.oid, rest_ticks]) + self.set_reg(REG_POWER_CTL, 0x08) logging.info("ADXL345 starting '%s' measurements", self.name) # Initialize clock tracking - self.last_sequence = 0 - self.last_limit_count = self.last_error_count = 0 - self.clock_sync.reset(reqclock, 0) - self.max_query_duration = 1 << 31 - self._update_clock(minclock=reqclock) - self.max_query_duration = 1 << 31 + self.clock_updater.note_start() + self.last_error_count = 0 def _finish_measurements(self): - if not self.is_measuring(): - return # Halt bulk reading - params = self.query_adxl345_end_cmd.send([self.oid, 0, 0]) - self.query_rate = 0 - with self.lock: - self.raw_samples = [] + self.set_reg(REG_POWER_CTL, 0x00) + self.query_adxl345_cmd.send_wait_ack([self.oid, 0]) + self.bulk_queue.clear_samples() logging.info("ADXL345 finished '%s' measurements", self.name) - # API interface - def _api_update(self, eventtime): - self._update_clock() - with self.lock: - raw_samples = self.raw_samples - self.raw_samples = [] + def _process_batch(self, eventtime): + self.clock_updater.update_clock() + raw_samples = self.bulk_queue.pull_samples() if not raw_samples: return {} samples = self._extract_samples(raw_samples) @@ -519,24 +421,9 @@ def _api_update(self, eventtime): return { "data": samples, "errors": self.last_error_count, - "overflows": self.last_limit_count, + "overflows": self.clock_updater.get_last_overflows(), } - def _api_startstop(self, is_start): - if is_start: - self._start_measurements() - else: - self._finish_measurements() - - def _handle_dump_adxl345(self, web_request): - self.api_dump.add_client(web_request) - hdr = ("time", "x_acceleration", "y_acceleration", "z_acceleration") - web_request.send({"header": hdr}) - - def start_internal_client(self): - cconn = self.api_dump.add_internal_client() - return AccelQueryHelper(self.printer, cconn) - def load_config(config): return ADXL345(config) diff --git a/klippy/extras/angle.py b/klippy/extras/angle.py index 0cfb5e687..56a9658a4 100644 --- a/klippy/extras/angle.py +++ b/klippy/extras/angle.py @@ -3,8 +3,8 @@ # Copyright (C) 2021,2022 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import logging, math, threading -from . import bus, motion_report +import logging, math +from . import bus, bulk_sensor import numpy MIN_MSG_TIME = 0.100 @@ -179,8 +179,16 @@ def get_stepper_phase(self): def do_calibration_moves(self): move = self.printer.lookup_object("force_move").manual_move # Start data collection - angle_sensor = self.printer.lookup_object(self.name) - cconn = angle_sensor.start_internal_client() + msgs = [] + is_finished = False + + def handle_batch(msg): + if is_finished: + return False + msgs.append(msg) + return True + + self.printer.lookup_object(self.name).add_client(handle_batch) # Move stepper several turns (to allow internal sensor calibration) microsteps, full_steps = self.get_microsteps() mcu_stepper = self.mcu_stepper @@ -212,13 +220,12 @@ def do_calibration_moves(self): move(mcu_stepper, 0.5 * rotation_dist + align_dist, move_speed) toolhead.wait_moves() # Finish data collection - cconn.finalize() - msgs = cconn.get_messages() + is_finished = True # Correlate query responses cal = {} step = 0 for msg in msgs: - for query_time, pos in msg["params"]["data"]: + for query_time, pos in msg["data"]: # Add to step tracking while step < len(times) and query_time > times[step][1]: step += 1 @@ -476,7 +483,11 @@ def cmd_ANGLE_DEBUG_WRITE(self, gcmd): self._write_reg(reg, val) +BYTES_PER_SAMPLE = 3 +SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE + SAMPLE_PERIOD = 0.000400 +BATCH_UPDATES = 0.100 class Angle: @@ -489,9 +500,6 @@ def __init__(self, config): # Measurement conversion self.start_clock = self.time_shift = self.sample_ticks = 0 self.last_sequence = self.last_angle = 0 - # Measurement storage (accessed from background thread) - self.lock = threading.Lock() - self.raw_samples = [] # Sensor type sensors = { "a1333": HelperA1333, @@ -507,7 +515,7 @@ def __init__(self, config): self.oid = oid = mcu.create_oid() self.sensor_helper = sensor_class(config, self.spi, oid) # Setup mcu sensor_spi_angle bulk query code - self.query_spi_angle_cmd = self.query_spi_angle_end_cmd = None + self.query_spi_angle_cmd = None mcu.add_config_cmd( "config_spi_angle oid=%d spi_oid=%d spi_angle_type=%s" % (oid, self.spi.get_oid(), sensor_type) @@ -517,17 +525,19 @@ def __init__(self, config): on_restart=True, ) mcu.register_config_callback(self._build_config) - mcu.register_response( - self._handle_spi_angle_data, "spi_angle_data", oid - ) - # API server endpoints - self.api_dump = motion_report.APIDumpHelper( - self.printer, self._api_update, self._api_startstop, 0.100 + self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=oid) + # Process messages in batches + self.batch_bulk = bulk_sensor.BatchBulkHelper( + self.printer, + self._process_batch, + self._start_measurements, + self._finish_measurements, + BATCH_UPDATES, ) self.name = config.get_name().split()[1] - wh = self.printer.lookup_object("webhooks") - wh.register_mux_endpoint( - "angle/dump_angle", "sensor", self.name, self._handle_dump_angle + api_resp = {"header": ("time", "angle")} + self.batch_bulk.add_mux_endpoint( + "angle/dump_angle", "sensor", self.name, api_resp ) def _build_config(self): @@ -539,24 +549,14 @@ def _build_config(self): "query_spi_angle oid=%c clock=%u rest_ticks=%u time_shift=%c", cq=cmdqueue, ) - self.query_spi_angle_end_cmd = self.mcu.lookup_query_command( - "query_spi_angle oid=%c clock=%u rest_ticks=%u time_shift=%c", - "spi_angle_end oid=%c sequence=%hu", - oid=self.oid, - cq=cmdqueue, - ) def get_status(self, eventtime=None): return {"temperature": self.sensor_helper.last_temperature} - # Measurement collection - def is_measuring(self): - return self.start_clock != 0 - - def _handle_spi_angle_data(self, params): - with self.lock: - self.raw_samples.append(params) + def add_client(self, client_cb): + self.batch_bulk.add_client(client_cb) + # Measurement decoding def _extract_samples(self, raw_samples): # Load variables to optimize inner loop below sample_ticks = self.sample_ticks @@ -577,18 +577,20 @@ def _extract_samples(self, raw_samples): static_delay = self.sensor_helper.get_static_delay() # Process every message in raw_samples count = error_count = 0 - samples = [None] * (len(raw_samples) * 16) + samples = [None] * (len(raw_samples) * SAMPLES_PER_BLOCK) for params in raw_samples: seq_diff = (params["sequence"] - last_sequence) & 0xFFFF last_sequence += seq_diff + samp_count = last_sequence * SAMPLES_PER_BLOCK + msg_mclock = start_clock + samp_count * sample_ticks d = bytearray(params["data"]) - msg_mclock = start_clock + last_sequence * 16 * sample_ticks - for i in range(len(d) // 3): - tcode = d[i * 3] + for i in range(len(d) // BYTES_PER_SAMPLE): + d_ta = d[i * BYTES_PER_SAMPLE : (i + 1) * BYTES_PER_SAMPLE] + tcode = d_ta[0] if tcode == TCODE_ERROR: error_count += 1 continue - raw_angle = d[i * 3 + 1] | (d[i * 3 + 2] << 8) + raw_angle = d_ta[1] | (d_ta[2] << 8) angle_diff = (raw_angle - last_angle) & 0xFFFF angle_diff -= (angle_diff & 0x8000) << 1 last_angle += angle_diff @@ -611,33 +613,15 @@ def _extract_samples(self, raw_samples): del samples[count:] return samples, error_count - # API interface - def _api_update(self, eventtime): - if self.sensor_helper.is_tcode_absolute: - self.sensor_helper.update_clock() - with self.lock: - raw_samples = self.raw_samples - self.raw_samples = [] - if not raw_samples: - return {} - samples, error_count = self._extract_samples(raw_samples) - if not samples: - return {} - offset = self.calibration.apply_calibration(samples) - return { - "data": samples, - "errors": error_count, - "position_offset": offset, - } + # Start, stop, and process message batches + def _is_measuring(self): + return self.start_clock != 0 def _start_measurements(self): - if self.is_measuring(): - return logging.info("Starting angle '%s' measurements", self.name) self.sensor_helper.start() # Start bulk reading - with self.lock: - self.raw_samples = [] + self.bulk_queue.clear_samples() self.last_sequence = 0 systime = self.printer.get_reactor().monotonic() print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME @@ -649,29 +633,27 @@ def _start_measurements(self): ) def _finish_measurements(self): - if not self.is_measuring(): - return # Halt bulk reading - params = self.query_spi_angle_end_cmd.send([self.oid, 0, 0, 0]) - self.start_clock = 0 - with self.lock: - self.raw_samples = [] + self.query_spi_angle_cmd.send_wait_ack([self.oid, 0, 0, 0]) + self.bulk_queue.clear_samples() self.sensor_helper.last_temperature = None logging.info("Stopped angle '%s' measurements", self.name) - def _api_startstop(self, is_start): - if is_start: - self._start_measurements() - else: - self._finish_measurements() - - def _handle_dump_angle(self, web_request): - self.api_dump.add_client(web_request) - hdr = ("time", "angle") - web_request.send({"header": hdr}) - - def start_internal_client(self): - return self.api_dump.add_internal_client() + def _process_batch(self, eventtime): + if self.sensor_helper.is_tcode_absolute: + self.sensor_helper.update_clock() + raw_samples = self.bulk_queue.pull_samples() + if not raw_samples: + return {} + samples, error_count = self._extract_samples(raw_samples) + if not samples: + return {} + offset = self.calibration.apply_calibration(samples) + return { + "data": samples, + "errors": error_count, + "position_offset": offset, + } def load_config_prefix(config): diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index f67786fff..30250f8e5 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -1,6 +1,5 @@ # Mesh Bed Leveling # -# Copyright (C) 2018 Kevin O'Connor # Copyright (C) 2018-2019 Eric Callahan # # This file may be distributed under the terms of the GNU GPLv3 license. @@ -350,6 +349,7 @@ def __init__(self, config, bedmesh): self.orig_config = {"radius": None, "origin": None} self.radius = self.origin = None self.mesh_min = self.mesh_max = (0.0, 0.0) + self.adaptive_margin = config.getfloat("adaptive_margin", 0.0) self.zero_ref_pos = config.getfloatlist( "zero_reference_position", None, count=2 ) @@ -672,6 +672,144 @@ def _verify_algorithm(self, error): ) params["algo"] = "lagrange" + def set_adaptive_mesh(self, gcmd): + if not gcmd.get_int("ADAPTIVE", 0): + return False + exclude_objects = self.printer.lookup_object("exclude_object", None) + if exclude_objects is None: + gcmd.respond_info("Exclude objects not enabled. Using full mesh...") + return False + objects = exclude_objects.get_status().get("objects", []) + if not objects: + return False + margin = gcmd.get_float("ADAPTIVE_MARGIN", self.adaptive_margin) + + # List all exclude_object points by axis and iterate over + # all polygon points, and pick the min and max or each axis + list_of_xs = [] + list_of_ys = [] + gcmd.respond_info("Found %s objects" % (len(objects))) + for obj in objects: + for point in obj["polygon"]: + list_of_xs.append(point[0]) + list_of_ys.append(point[1]) + + # Define bounds of adaptive mesh area + mesh_min = [min(list_of_xs), min(list_of_ys)] + mesh_max = [max(list_of_xs), max(list_of_ys)] + adjusted_mesh_min = [x - margin for x in mesh_min] + adjusted_mesh_max = [x + margin for x in mesh_max] + + # Force margin to respect original mesh bounds + adjusted_mesh_min[0] = max( + adjusted_mesh_min[0], self.orig_config["mesh_min"][0] + ) + adjusted_mesh_min[1] = max( + adjusted_mesh_min[1], self.orig_config["mesh_min"][1] + ) + adjusted_mesh_max[0] = min( + adjusted_mesh_max[0], self.orig_config["mesh_max"][0] + ) + adjusted_mesh_max[1] = min( + adjusted_mesh_max[1], self.orig_config["mesh_max"][1] + ) + + adjusted_mesh_size = ( + adjusted_mesh_max[0] - adjusted_mesh_min[0], + adjusted_mesh_max[1] - adjusted_mesh_min[1], + ) + + # Compute a ratio between the adapted and original sizes + ratio = ( + adjusted_mesh_size[0] + / ( + self.orig_config["mesh_max"][0] + - self.orig_config["mesh_min"][0] + ), + adjusted_mesh_size[1] + / ( + self.orig_config["mesh_max"][1] + - self.orig_config["mesh_min"][1] + ), + ) + + gcmd.respond_info( + "Original mesh bounds: (%s,%s)" + % (self.orig_config["mesh_min"], self.orig_config["mesh_max"]) + ) + gcmd.respond_info( + "Original probe count: (%s,%s)" + % (self.mesh_config["x_count"], self.mesh_config["y_count"]) + ) + gcmd.respond_info( + "Adapted mesh bounds: (%s,%s)" + % (adjusted_mesh_min, adjusted_mesh_max) + ) + gcmd.respond_info("Ratio: (%s, %s)" % ratio) + + new_x_probe_count = int( + math.ceil(self.mesh_config["x_count"] * ratio[0]) + ) + new_y_probe_count = int( + math.ceil(self.mesh_config["y_count"] * ratio[1]) + ) + + # There is one case, where we may have to adjust the probe counts: + # axis0 < 4 and axis1 > 6 (see _verify_algorithm). + min_num_of_probes = 3 + if ( + max(new_x_probe_count, new_y_probe_count) > 6 + and min(new_x_probe_count, new_y_probe_count) < 4 + ): + min_num_of_probes = 4 + + new_x_probe_count = max(min_num_of_probes, new_x_probe_count) + new_y_probe_count = max(min_num_of_probes, new_y_probe_count) + + gcmd.respond_info( + "Adapted probe count: (%s,%s)" + % (new_x_probe_count, new_y_probe_count) + ) + + # If the adapted mesh size is too small, adjust it to something + # useful. + adjusted_mesh_size = ( + max(adjusted_mesh_size[0], new_x_probe_count), + max(adjusted_mesh_size[1], new_y_probe_count), + ) + + if self.radius is not None: + adapted_radius = ( + math.sqrt( + (adjusted_mesh_size[0] ** 2) + (adjusted_mesh_size[1] ** 2) + ) + / 2 + ) + adapted_origin = ( + adjusted_mesh_min[0] + (adjusted_mesh_size[0] / 2), + adjusted_mesh_min[1] + (adjusted_mesh_size[1] / 2), + ) + to_adapted_origin = math.sqrt( + adapted_origin[0] ** 2 + adapted_origin[1] ** 2 + ) + # If the adapted mesh size is smaller than the default/full + # mesh, adjust the parameters. Otherwise, just do the full mesh. + if adapted_radius + to_adapted_origin < self.radius: + self.radius = adapted_radius + self.origin = adapted_origin + self.mesh_min = (-self.radius, -self.radius) + self.mesh_max = (self.radius, self.radius) + self.mesh_config["x_count"] = self.mesh_config["y_count"] = max( + new_x_probe_count, new_y_probe_count + ) + else: + self.mesh_min = adjusted_mesh_min + self.mesh_max = adjusted_mesh_max + self.mesh_config["x_count"] = new_x_probe_count + self.mesh_config["y_count"] = new_y_probe_count + self._profile_name = None + return True + def update_config(self, gcmd): # reset default configuration self.radius = self.orig_config["radius"] @@ -715,6 +853,8 @@ def update_config(self, gcmd): self.mesh_config["algo"] = gcmd.get("ALGORITHM").strip().lower() need_cfg_update = True + need_cfg_update |= self.set_adaptive_mesh(gcmd) + if need_cfg_update: self._verify_algorithm(gcmd.error) self._generate_points(gcmd.error) @@ -896,7 +1036,8 @@ def probe_finalize(self, offsets, positions): z_mesh.set_zero_reference(*self.zero_ref_pos) self.bedmesh.set_mesh(z_mesh) self.gcode.respond_info("Mesh Bed Leveling Complete") - self.bedmesh.save_profile(self._profile_name) + if self._profile_name is not None: + self.bedmesh.save_profile(self._profile_name) def _dump_points(self, probed_pts, corrected_pts, offsets): # logs generated points with offset applied, points received diff --git a/klippy/extras/bulk_sensor.py b/klippy/extras/bulk_sensor.py new file mode 100644 index 000000000..71fd64c05 --- /dev/null +++ b/klippy/extras/bulk_sensor.py @@ -0,0 +1,307 @@ +# Tools for reading bulk sensor data from the mcu +# +# Copyright (C) 2020-2023 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging, threading + +# This "bulk sensor" module facilitates the processing of sensor chip +# measurements that do not require the host to respond with low +# latency. This module helps collect these measurements into batches +# that are then processed periodically by the host code (as specified +# by BatchBulkHelper.batch_interval). It supports the collection of +# thousands of sensor measurements per second. +# +# Processing measurements in batches reduces load on the mcu, reduces +# bandwidth to/from the mcu, and reduces load on the host. It also +# makes it easier to export the raw measurements via the webhooks +# system (aka API Server). + +BATCH_INTERVAL = 0.500 + + +# Helper to process accumulated messages in periodic batches +class BatchBulkHelper: + def __init__( + self, + printer, + batch_cb, + start_cb=None, + stop_cb=None, + batch_interval=BATCH_INTERVAL, + ): + self.printer = printer + self.batch_cb = batch_cb + if start_cb is None: + + def start_cb(): + return None + + self.start_cb = start_cb + if stop_cb is None: + + def stop_cb(): + return None + + self.stop_cb = stop_cb + self.is_started = False + self.batch_interval = batch_interval + self.batch_timer = None + self.client_cbs = [] + self.webhooks_start_resp = {} + + # Periodic batch processing + def _start(self): + if self.is_started: + return + self.is_started = True + try: + self.start_cb() + except self.printer.command_error as e: + logging.exception("BatchBulkHelper start callback error") + self.is_started = False + del self.client_cbs[:] + raise + reactor = self.printer.get_reactor() + systime = reactor.monotonic() + waketime = systime + self.batch_interval + self.batch_timer = reactor.register_timer(self._proc_batch, waketime) + + def _stop(self): + del self.client_cbs[:] + self.printer.get_reactor().unregister_timer(self.batch_timer) + self.batch_timer = None + if not self.is_started: + return + try: + self.stop_cb() + except self.printer.command_error as e: + logging.exception("BatchBulkHelper stop callback error") + del self.client_cbs[:] + self.is_started = False + if self.client_cbs: + # New client started while in process of stopping + self._start() + + def _proc_batch(self, eventtime): + try: + msg = self.batch_cb(eventtime) + except self.printer.command_error as e: + logging.exception("BatchBulkHelper batch callback error") + self._stop() + return self.printer.get_reactor().NEVER + if not msg: + return eventtime + self.batch_interval + for client_cb in list(self.client_cbs): + res = client_cb(msg) + if not res: + # This client no longer needs updates - unregister it + self.client_cbs.remove(client_cb) + if not self.client_cbs: + self._stop() + return self.printer.get_reactor().NEVER + return eventtime + self.batch_interval + + # Client registration + def add_client(self, client_cb): + self.client_cbs.append(client_cb) + self._start() + + # Webhooks registration + def _add_api_client(self, web_request): + whbatch = BatchWebhooksClient(web_request) + self.add_client(whbatch.handle_batch) + web_request.send(self.webhooks_start_resp) + + def add_mux_endpoint(self, path, key, value, webhooks_start_resp): + self.webhooks_start_resp = webhooks_start_resp + wh = self.printer.lookup_object("webhooks") + wh.register_mux_endpoint(path, key, value, self._add_api_client) + + +# A webhooks wrapper for use by BatchBulkHelper +class BatchWebhooksClient: + def __init__(self, web_request): + self.cconn = web_request.get_client_connection() + self.template = web_request.get_dict("response_template", {}) + + def handle_batch(self, msg): + if self.cconn.is_closed(): + return False + tmp = dict(self.template) + tmp["params"] = msg + self.cconn.send(tmp) + return True + + +# Helper class to store incoming messages in a queue +class BulkDataQueue: + def __init__(self, mcu, msg_name="sensor_bulk_data", oid=None): + # Measurement storage (accessed from background thread) + self.lock = threading.Lock() + self.raw_samples = [] + # Register callback with mcu + mcu.register_response(self._handle_data, msg_name, oid) + + def _handle_data(self, params): + with self.lock: + self.raw_samples.append(params) + + def pull_samples(self): + with self.lock: + raw_samples = self.raw_samples + self.raw_samples = [] + return raw_samples + + def clear_samples(self): + self.pull_samples() + + +###################################################################### +# Clock synchronization +###################################################################### + +# It is common for sensors to produce measurements at a fixed +# frequency. If the mcu can reliably obtain all of these +# measurements, then the code here can calculate a precision timestamp +# for them. That is, it can determine the actual sensor measurement +# frequency, the time of the first measurement, and thus a precise +# time for all measurements. +# +# This system works by having the mcu periodically report a precision +# timestamp along with the total number of measurements the sensor has +# taken as of that time. In brief, knowing the total number of +# measurements taken over an extended period provides an accurate +# estimate of measurement frequency, which can then also be utilized +# to determine the time of the first measurement. + + +# Helper class for chip clock synchronization via linear regression +class ClockSyncRegression: + def __init__(self, mcu, chip_clock_smooth, decay=1.0 / 20.0): + self.mcu = mcu + self.chip_clock_smooth = chip_clock_smooth + self.decay = decay + self.last_chip_clock = self.last_exp_mcu_clock = 0.0 + self.mcu_clock_avg = self.mcu_clock_variance = 0.0 + self.chip_clock_avg = self.chip_clock_covariance = 0.0 + + def reset(self, mcu_clock, chip_clock): + self.mcu_clock_avg = self.last_mcu_clock = mcu_clock + self.chip_clock_avg = chip_clock + self.mcu_clock_variance = self.chip_clock_covariance = 0.0 + self.last_chip_clock = self.last_exp_mcu_clock = 0.0 + + def update(self, mcu_clock, chip_clock): + # Update linear regression + decay = self.decay + diff_mcu_clock = mcu_clock - self.mcu_clock_avg + self.mcu_clock_avg += decay * diff_mcu_clock + self.mcu_clock_variance = (1.0 - decay) * ( + self.mcu_clock_variance + diff_mcu_clock**2 * decay + ) + diff_chip_clock = chip_clock - self.chip_clock_avg + self.chip_clock_avg += decay * diff_chip_clock + self.chip_clock_covariance = (1.0 - decay) * ( + self.chip_clock_covariance + + diff_mcu_clock * diff_chip_clock * decay + ) + + def set_last_chip_clock(self, chip_clock): + base_mcu, base_chip, inv_cfreq = self.get_clock_translation() + self.last_chip_clock = chip_clock + self.last_exp_mcu_clock = ( + base_mcu + (chip_clock - base_chip) * inv_cfreq + ) + + def get_clock_translation(self): + inv_chip_freq = self.mcu_clock_variance / self.chip_clock_covariance + if not self.last_chip_clock: + return self.mcu_clock_avg, self.chip_clock_avg, inv_chip_freq + # Find mcu clock associated with future chip_clock + s_chip_clock = self.last_chip_clock + self.chip_clock_smooth + scdiff = s_chip_clock - self.chip_clock_avg + s_mcu_clock = self.mcu_clock_avg + scdiff * inv_chip_freq + # Calculate frequency to converge at future point + mdiff = s_mcu_clock - self.last_exp_mcu_clock + s_inv_chip_freq = mdiff / self.chip_clock_smooth + return self.last_exp_mcu_clock, self.last_chip_clock, s_inv_chip_freq + + def get_time_translation(self): + base_mcu, base_chip, inv_cfreq = self.get_clock_translation() + clock_to_print_time = self.mcu.clock_to_print_time + base_time = clock_to_print_time(base_mcu) + inv_freq = clock_to_print_time(base_mcu + inv_cfreq) - base_time + return base_time, base_chip, inv_freq + + +MAX_BULK_MSG_SIZE = 52 + + +# Handle common periodic chip status query responses +class ChipClockUpdater: + def __init__(self, clock_sync, bytes_per_sample): + self.clock_sync = clock_sync + self.bytes_per_sample = bytes_per_sample + self.samples_per_block = MAX_BULK_MSG_SIZE // bytes_per_sample + self.last_sequence = self.max_query_duration = 0 + self.last_overflows = 0 + self.mcu = self.oid = self.query_status_cmd = None + + def setup_query_command(self, mcu, msgformat, oid, cq): + self.mcu = mcu + self.oid = oid + self.query_status_cmd = self.mcu.lookup_query_command( + msgformat, + "sensor_bulk_status oid=%c clock=%u query_ticks=%u" + " next_sequence=%hu buffered=%u possible_overflows=%hu", + oid=oid, + cq=cq, + ) + + def get_last_sequence(self): + return self.last_sequence + + def get_last_overflows(self): + return self.last_overflows + + def clear_duration_filter(self): + self.max_query_duration = 1 << 31 + + def note_start(self): + self.last_sequence = 0 + self.last_overflows = 0 + # Set initial clock + self.clear_duration_filter() + self.update_clock(is_reset=True) + self.clear_duration_filter() + + def update_clock(self, is_reset=False): + params = self.query_status_cmd.send([self.oid]) + mcu_clock = self.mcu.clock32_to_clock64(params["clock"]) + seq_diff = (params["next_sequence"] - self.last_sequence) & 0xFFFF + self.last_sequence += seq_diff + buffered = params["buffered"] + po_diff = (params["possible_overflows"] - self.last_overflows) & 0xFFFF + self.last_overflows += po_diff + duration = params["query_ticks"] + if duration > self.max_query_duration: + # Skip measurement as a high query time could skew clock tracking + self.max_query_duration = max( + 2 * self.max_query_duration, self.mcu.seconds_to_clock(0.000005) + ) + return + self.max_query_duration = 2 * duration + msg_count = ( + self.last_sequence * self.samples_per_block + + buffered // self.bytes_per_sample + ) + # The "chip clock" is the message counter plus .5 for average + # inaccuracy of query responses and plus .5 for assumed offset + # of hardware processing time. + chip_clock = msg_count + 1 + avg_mcu_clock = mcu_clock + duration // 2 + if is_reset: + self.clock_sync.reset(avg_mcu_clock, chip_clock) + else: + self.clock_sync.update(avg_mcu_clock, chip_clock) diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index e4415eb76..6b709dcc4 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -1,6 +1,6 @@ # Support for button detection and callbacks # -# Copyright (C) 2018 Kevin O'Connor +# Copyright (C) 2018-2023 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. @@ -79,19 +79,18 @@ def handle_buttons_state(self, params): # Send ack to MCU self.ack_cmd.send([self.oid, new_count]) self.ack_count += new_count - # Call self.handle_button() with this event in main thread - for nb in new_buttons: - self.reactor.register_async_callback( - (lambda e, s=self, b=nb: s.handle_button(e, b)) - ) - - def handle_button(self, eventtime, button): - button ^= self.invert - changed = button ^ self.last_button - for mask, shift, callback in self.callbacks: - if changed & mask: - callback(eventtime, (button & mask) >> shift) - self.last_button = button + # Invoke callbacks with this event in main thread + btime = params["#receive_time"] + for button in new_buttons: + button ^= self.invert + changed = button ^ self.last_button + self.last_button = button + for mask, shift, callback in self.callbacks: + if changed & mask: + state = (button & mask) >> shift + self.reactor.register_async_callback( + (lambda et, c=callback, bt=btime, s=state: c(bt, s)) + ) ###################################################################### @@ -171,6 +170,7 @@ def call_button(self, button, state): # Rotary Encoders ###################################################################### + # Rotary encoder handler https://github.com/brianlow/Rotary # Copyright 2011 Ben Buxton (bb@cactii.net). # Licenced under the GNU GPL Version 3. diff --git a/klippy/extras/danger_options.py b/klippy/extras/danger_options.py index 71774a278..7c838f8d2 100644 --- a/klippy/extras/danger_options.py +++ b/klippy/extras/danger_options.py @@ -22,6 +22,7 @@ def __init__(self, config): self.homing_elapsed_distance_tolerance = config.getfloat( "homing_elapsed_distance_tolerance", 0.5, minval=0.0 ) + self.adc_ignore_limits = config.getboolean("adc_ignore_limits", False) def load_config(config): diff --git a/klippy/extras/dockable_probe.py b/klippy/extras/dockable_probe.py index b4f09d009..42ce87022 100644 --- a/klippy/extras/dockable_probe.py +++ b/klippy/extras/dockable_probe.py @@ -203,7 +203,6 @@ def __init__(self, config): pin = config.get("pin") pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) mcu = pin_params["chip"] - mcu.register_config_callback(self._build_config) self.mcu_endstop = mcu.setup_pin("endstop", pin_params) # Wrappers @@ -283,6 +282,10 @@ def __init__(self, config): "klippy:connect", self._handle_connect ) + self.printer.register_event_handler( + "klippy:mcu_identify", self._handle_config + ) + # Parse a string coordinate representation from the config # and return a list of numbers. # @@ -307,7 +310,7 @@ def _parse_coord(self, config, name, expected_dims=3): p[:supplied_dims] = vals return p - def _build_config(self): + def _handle_config(self): kin = self.printer.lookup_object("toolhead").get_kinematics() for stepper in kin.get_steppers(): if stepper.is_active_axis("z"): diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index f55c291ad..d4e0bdfa9 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -12,6 +12,7 @@ BUZZ_RADIANS_VELOCITY = BUZZ_RADIANS_DISTANCE / 0.250 STALL_TIME = 0.100 + # Calculate a move's accel_t, cruise_t, and cruise_v def calc_move_time(dist, speed, accel): axis_r = 1.0 @@ -113,11 +114,14 @@ def manual_move(self, stepper, dist, speed, accel=0.0): ) print_time = print_time + accel_t + cruise_t + accel_t stepper.generate_steps(print_time) - self.trapq_finalize_moves(self.trapq, print_time + 99999.9) + self.trapq_finalize_moves( + self.trapq, print_time + 99999.9, print_time + 99999.9 + ) stepper.set_trapq(prev_trapq) stepper.set_stepper_kinematics(prev_sk) - toolhead.note_kinematic_activity(print_time) + toolhead.note_mcu_movequeue_activity(print_time) toolhead.dwell(accel_t + cruise_t + accel_t) + toolhead.flush_step_generation() def _lookup_stepper(self, gcmd): name = gcmd.get("STEPPER") diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index adfd9d062..1f633c2c6 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -259,10 +259,15 @@ def _set_current_homing(self, homing_axes): affected_rails = affected_rails | set(partial_rails) for rail in affected_rails: - ch = rail.get_tmc_current_helper() - if ch is not None and ch.needs_home_current_change(): - ch.set_current_for_homing(print_time) - self.toolhead.dwell(ch.current_change_dwell_time) + chs = rail.get_tmc_current_helpers() + dwell_time = None + for ch in chs: + if ch is not None and ch.needs_home_current_change(): + if dwell_time is None: + dwell_time = ch.current_change_dwell_time + ch.set_current_for_homing(print_time) + if dwell_time: + self.toolhead.dwell(dwell_time) def _set_current_post_homing(self, homing_axes): print_time = self.toolhead.get_last_move_time() @@ -273,10 +278,15 @@ def _set_current_post_homing(self, homing_axes): affected_rails = affected_rails | set(partial_rails) for rail in affected_rails: - ch = rail.get_tmc_current_helper() - if ch is not None and ch.needs_home_current_change(): - ch.set_current_for_normal(print_time) - self.toolhead.dwell(ch.current_change_dwell_time) + chs = rail.get_tmc_current_helpers() + dwell_time = None + for ch in chs: + if ch is not None and ch.needs_home_current_change(): + if dwell_time is None: + dwell_time = ch.current_change_dwell_time + ch.set_current_for_normal(print_time) + if dwell_time: + self.toolhead.dwell(dwell_time) def home_rails(self, rails, forcepos, movepos): # Notify of upcoming homing operation @@ -303,7 +313,7 @@ def home_rails(self, rails, forcepos, movepos): # Perform second home if retract_dist: - logging.info("needs rehome: %s", needs_rehome) + logging.info("homing:needs rehome: %s", needs_rehome) # Retract startpos = self._fill_coord(forcepos) homepos = self._fill_coord(movepos) diff --git a/klippy/extras/lis2dw.py b/klippy/extras/lis2dw.py index dd23c672d..5e72ee5b0 100644 --- a/klippy/extras/lis2dw.py +++ b/klippy/extras/lis2dw.py @@ -1,13 +1,11 @@ # Support for reading acceleration data from an LIS2DW chip # # Copyright (C) 2023 Zhou.XianMing -# Copyright (C) 2020-2021 Kevin O'Connor +# Copyright (C) 2020-2023 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import collections import logging -import threading -from . import bus, motion_report, adxl345 +from . import bus, adxl345, bulk_sensor # LIS2DW registers REG_LIS2DW_WHO_AM_I_ADDR = 0x0F @@ -32,83 +30,60 @@ FREEFALL_ACCEL = 9.80665 SCALE = FREEFALL_ACCEL * 1.952 / 4 -Accel_Measurement = collections.namedtuple( - "Accel_Measurement", ("time", "accel_x", "accel_y", "accel_z") -) +BYTES_PER_SAMPLE = 6 +SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE -MIN_MSG_TIME = 0.100 +BATCH_UPDATES = 0.100 -BYTES_PER_SAMPLE = 6 -SAMPLES_PER_BLOCK = 8 # Printer class that controls LIS2DW chip class LIS2DW: def __init__(self, config): self.printer = config.get_printer() adxl345.AccelCommandHelper(config, self) - self.query_rate = 0 - am = { - "x": (0, SCALE), - "y": (1, SCALE), - "z": (2, SCALE), - "-x": (0, -SCALE), - "-y": (1, -SCALE), - "-z": (2, -SCALE), - } - axes_map = config.getlist("axes_map", ("x", "y", "z"), count=3) - if any([a not in am for a in axes_map]): - raise config.error("Invalid lis2dw axes_map parameter") - self.axes_map = [am[a.strip()] for a in axes_map] + self.axes_map = adxl345.read_axes_map(config) self.data_rate = 1600 - # Measurement storage (accessed from background thread) - self.lock = threading.Lock() - self.raw_samples = [] # Setup mcu sensor_lis2dw bulk query code self.spi = bus.MCU_SPI_from_config(config, 3, default_speed=5000000) self.mcu = mcu = self.spi.get_mcu() self.oid = oid = mcu.create_oid() - self.query_lis2dw_cmd = self.query_lis2dw_end_cmd = None - self.query_lis2dw_status_cmd = None + self.query_lis2dw_cmd = None mcu.add_config_cmd( "config_lis2dw oid=%d spi_oid=%d" % (oid, self.spi.get_oid()) ) mcu.add_config_cmd( - "query_lis2dw oid=%d clock=0 rest_ticks=0" % (oid,), on_restart=True + "query_lis2dw oid=%d rest_ticks=0" % (oid,), on_restart=True ) mcu.register_config_callback(self._build_config) - mcu.register_response(self._handle_lis2dw_data, "lis2dw_data", oid) + self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=oid) # Clock tracking - self.last_sequence = self.max_query_duration = 0 - self.last_limit_count = self.last_error_count = 0 - self.clock_sync = adxl345.ClockSyncRegression(self.mcu, 640) - # API server endpoints - self.api_dump = motion_report.APIDumpHelper( - self.printer, self._api_update, self._api_startstop, 0.100 + chip_smooth = self.data_rate * BATCH_UPDATES * 2 + self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth) + self.clock_updater = bulk_sensor.ChipClockUpdater( + self.clock_sync, BYTES_PER_SAMPLE + ) + self.last_error_count = 0 + # Process messages in batches + self.batch_bulk = bulk_sensor.BatchBulkHelper( + self.printer, + self._process_batch, + self._start_measurements, + self._finish_measurements, + BATCH_UPDATES, ) self.name = config.get_name().split()[-1] - wh = self.printer.lookup_object("webhooks") - wh.register_mux_endpoint( - "lis2dw/dump_lis2dw", "sensor", self.name, self._handle_dump_lis2dw + hdr = ("time", "x_acceleration", "y_acceleration", "z_acceleration") + self.batch_bulk.add_mux_endpoint( + "lis2dw/dump_lis2dw", "sensor", self.name, {"header": hdr} ) def _build_config(self): cmdqueue = self.spi.get_command_queue() self.query_lis2dw_cmd = self.mcu.lookup_command( - "query_lis2dw oid=%c clock=%u rest_ticks=%u", cq=cmdqueue + "query_lis2dw oid=%c rest_ticks=%u", cq=cmdqueue ) - self.query_lis2dw_end_cmd = self.mcu.lookup_query_command( - "query_lis2dw oid=%c clock=%u rest_ticks=%u", - "lis2dw_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%c limit_count=%hu", - oid=self.oid, - cq=cmdqueue, - ) - self.query_lis2dw_status_cmd = self.mcu.lookup_query_command( - "query_lis2dw_status oid=%c", - "lis2dw_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%c limit_count=%hu", - oid=self.oid, - cq=cmdqueue, + self.clock_updater.setup_query_command( + self.mcu, "query_lis2dw_status oid=%c", oid=self.oid, cq=cmdqueue ) def read_reg(self, reg): @@ -127,18 +102,16 @@ def set_reg(self, reg, val, minclock=0): % (reg, val, stored_val) ) - # Measurement collection - def is_measuring(self): - return self.query_rate > 0 - - def _handle_lis2dw_data(self, params): - with self.lock: - self.raw_samples.append(params) + def start_internal_client(self): + aqh = adxl345.AccelQueryHelper(self.printer) + self.batch_bulk.add_client(aqh.handle_batch) + return aqh + # Measurement decoding def _extract_samples(self, raw_samples): # Load variables to optimize inner loop below (x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map - last_sequence = self.last_sequence + last_sequence = self.clock_updater.get_last_sequence() time_base, chip_base, inv_freq = self.clock_sync.get_time_translation() # Process every message in raw_samples count = seq = 0 @@ -172,45 +145,8 @@ def _extract_samples(self, raw_samples): del samples[count:] return samples - def _update_clock(self, minclock=0): - # Query current state - for retry in range(5): - params = self.query_lis2dw_status_cmd.send( - [self.oid], minclock=minclock - ) - fifo = params["fifo"] & 0x1F - if fifo <= 32: - break - else: - raise self.printer.command_error("Unable to query lis2dw fifo") - mcu_clock = self.mcu.clock32_to_clock64(params["clock"]) - seq_diff = (params["next_sequence"] - self.last_sequence) & 0xFFFF - self.last_sequence += seq_diff - buffered = params["buffered"] - lc_diff = (params["limit_count"] - self.last_limit_count) & 0xFFFF - self.last_limit_count += lc_diff - duration = params["query_ticks"] - if duration > self.max_query_duration: - # Skip measurement as a high query time could skew clock tracking - self.max_query_duration = max( - 2 * self.max_query_duration, self.mcu.seconds_to_clock(0.000005) - ) - return - self.max_query_duration = 2 * duration - msg_count = ( - self.last_sequence * SAMPLES_PER_BLOCK - + buffered // BYTES_PER_SAMPLE - + fifo - ) - # The "chip clock" is the message counter plus .5 for average - # inaccuracy of query responses and plus .5 for assumed offset - # of lis2dw hw processing time. - chip_clock = msg_count + 1 - self.clock_sync.update(mcu_clock + duration // 2, chip_clock) - + # Start, stop, and process message batches def _start_measurements(self): - if self.is_measuring(): - return # In case of miswiring, testing LIS2DW device ID prevents treating # noise or wrong signal as a correctly initialized device dev_id = self.read_reg(REG_LIS2DW_WHO_AM_I_ADDR) @@ -232,44 +168,27 @@ def _start_measurements(self): # High-Performance Mode (14-bit resolution) self.set_reg(REG_LIS2DW_CTRL_REG1_ADDR, 0x94) - # Setup samples - with self.lock: - self.raw_samples = [] # Start bulk reading - systime = self.printer.get_reactor().monotonic() - print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME - reqclock = self.mcu.print_time_to_clock(print_time) + self.bulk_queue.clear_samples() rest_ticks = self.mcu.seconds_to_clock(4.0 / self.data_rate) - self.query_rate = self.data_rate - self.query_lis2dw_cmd.send( - [self.oid, reqclock, rest_ticks], reqclock=reqclock - ) + self.query_lis2dw_cmd.send([self.oid, rest_ticks]) + self.set_reg(REG_LIS2DW_FIFO_CTRL, 0xC0) logging.info("LIS2DW starting '%s' measurements", self.name) # Initialize clock tracking - self.last_sequence = 0 - self.last_limit_count = self.last_error_count = 0 - self.clock_sync.reset(reqclock, 0) - self.max_query_duration = 1 << 31 - self._update_clock(minclock=reqclock) - self.max_query_duration = 1 << 31 + self.clock_updater.note_start() + self.last_error_count = 0 def _finish_measurements(self): - if not self.is_measuring(): - return # Halt bulk reading - params = self.query_lis2dw_end_cmd.send([self.oid, 0, 0]) - self.query_rate = 0 - with self.lock: - self.raw_samples = [] + self.set_reg(REG_LIS2DW_FIFO_CTRL, 0x00) + self.query_lis2dw_cmd.send_wait_ack([self.oid, 0]) + self.bulk_queue.clear_samples() logging.info("LIS2DW finished '%s' measurements", self.name) self.set_reg(REG_LIS2DW_FIFO_CTRL, 0x00) - # API interface - def _api_update(self, eventtime): - self._update_clock() - with self.lock: - raw_samples = self.raw_samples - self.raw_samples = [] + def _process_batch(self, eventtime): + self.clock_updater.update_clock() + raw_samples = self.bulk_queue.pull_samples() if not raw_samples: return {} samples = self._extract_samples(raw_samples) @@ -278,24 +197,9 @@ def _api_update(self, eventtime): return { "data": samples, "errors": self.last_error_count, - "overflows": self.last_limit_count, + "overflows": self.clock_updater.get_last_overflows(), } - def _api_startstop(self, is_start): - if is_start: - self._start_measurements() - else: - self._finish_measurements() - - def _handle_dump_lis2dw(self, web_request): - self.api_dump.add_client(web_request) - hdr = ("time", "x_acceleration", "y_acceleration", "z_acceleration") - web_request.send({"header": hdr}) - - def start_internal_client(self): - cconn = self.api_dump.add_internal_client() - return adxl345.AccelQueryHelper(self.printer, cconn) - def load_config(config): return LIS2DW(config) diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index ce8aed20f..c667ecf9c 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -92,9 +92,13 @@ def do_move(self, movepos, speed, accel, sync=True): ) self.next_cmd_time = self.next_cmd_time + accel_t + cruise_t + accel_t self.rail.generate_steps(self.next_cmd_time) - self.trapq_finalize_moves(self.trapq, self.next_cmd_time + 99999.9) + self.trapq_finalize_moves( + self.trapq, + self.next_cmd_time + 99999.9, + self.next_cmd_time + 99999.9, + ) toolhead = self.printer.lookup_object("toolhead") - toolhead.note_kinematic_activity(self.next_cmd_time) + toolhead.note_mcu_movequeue_activity(self.next_cmd_time) if sync: self.sync_print_time() diff --git a/klippy/extras/motion_report.py b/klippy/extras/motion_report.py index 5acdc47f3..f7f9df02d 100644 --- a/klippy/extras/motion_report.py +++ b/klippy/extras/motion_report.py @@ -5,117 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging import chelper - -API_UPDATE_INTERVAL = 0.500 - -# Helper to periodically transmit data to a set of API clients -class APIDumpHelper: - def __init__( - self, - printer, - data_cb, - startstop_cb=None, - update_interval=API_UPDATE_INTERVAL, - ): - self.printer = printer - self.data_cb = data_cb - if startstop_cb is None: - - def startstop_cb(is_start): - return None - - self.startstop_cb = startstop_cb - self.is_started = False - self.update_interval = update_interval - self.update_timer = None - self.clients = {} - - def _stop(self): - self.clients.clear() - reactor = self.printer.get_reactor() - reactor.unregister_timer(self.update_timer) - self.update_timer = None - if not self.is_started: - return reactor.NEVER - try: - self.startstop_cb(False) - except self.printer.command_error as e: - logging.exception("API Dump Helper stop callback error") - self.clients.clear() - self.is_started = False - if self.clients: - # New client started while in process of stopping - self._start() - return reactor.NEVER - - def _start(self): - if self.is_started: - return - self.is_started = True - try: - self.startstop_cb(True) - except self.printer.command_error as e: - logging.exception("API Dump Helper start callback error") - self.is_started = False - self.clients.clear() - raise - reactor = self.printer.get_reactor() - systime = reactor.monotonic() - waketime = systime + self.update_interval - self.update_timer = reactor.register_timer(self._update, waketime) - - def add_client(self, web_request): - cconn = web_request.get_client_connection() - template = web_request.get_dict("response_template", {}) - self.clients[cconn] = template - self._start() - - def add_internal_client(self): - cconn = InternalDumpClient() - self.clients[cconn] = {} - self._start() - return cconn - - def _update(self, eventtime): - try: - msg = self.data_cb(eventtime) - except self.printer.command_error as e: - logging.exception("API Dump Helper data callback error") - return self._stop() - if not msg: - return eventtime + self.update_interval - for cconn, template in list(self.clients.items()): - if cconn.is_closed(): - del self.clients[cconn] - if not self.clients: - return self._stop() - continue - tmp = dict(template) - tmp["params"] = msg - cconn.send(tmp) - return eventtime + self.update_interval - - -# An "internal webhooks" wrapper for using APIDumpHelper internally -class InternalDumpClient: - def __init__(self): - self.msgs = [] - self.is_done = False - - def get_messages(self): - return self.msgs - - def finalize(self): - self.is_done = True - - def is_closed(self): - return self.is_done - - def send(self, msg): - self.msgs.append(msg) - if len(self.msgs) >= 10000: - # Avoid filling up memory with too many samples - self.finalize() +from . import bulk_sensor # Extract stepper queue_step messages @@ -123,14 +13,16 @@ class DumpStepper: def __init__(self, printer, mcu_stepper): self.printer = printer self.mcu_stepper = mcu_stepper - self.last_api_clock = 0 - self.api_dump = APIDumpHelper(printer, self._api_update) - wh = self.printer.lookup_object("webhooks") - wh.register_mux_endpoint( + self.last_batch_clock = 0 + self.batch_bulk = bulk_sensor.BatchBulkHelper( + printer, self._process_batch + ) + api_resp = {"header": ("interval", "count", "add")} + self.batch_bulk.add_mux_endpoint( "motion_report/dump_stepper", "name", mcu_stepper.get_name(), - self._add_api_client, + api_resp, ) def get_step_queue(self, start_clock, end_clock): @@ -173,15 +65,15 @@ def log_steps(self, data): ) logging.info("\n".join(out)) - def _api_update(self, eventtime): - data, cdata = self.get_step_queue(self.last_api_clock, 1 << 63) + def _process_batch(self, eventtime): + data, cdata = self.get_step_queue(self.last_batch_clock, 1 << 63) if not data: return {} clock_to_print_time = self.mcu_stepper.get_mcu().clock_to_print_time first = data[0] first_clock = first.first_clock first_time = clock_to_print_time(first_clock) - self.last_api_clock = last_clock = data[-1].last_clock + self.last_batch_clock = last_clock = data[-1].last_clock last_time = clock_to_print_time(last_clock) mcu_pos = first.start_position start_position = self.mcu_stepper.mcu_to_commanded_position(mcu_pos) @@ -200,25 +92,32 @@ def _api_update(self, eventtime): "last_step_time": last_time, } - def _add_api_client(self, web_request): - self.api_dump.add_client(web_request) - hdr = ("interval", "count", "add") - web_request.send({"header": hdr}) - NEVER_TIME = 9999999999999999.0 + # Extract trapezoidal motion queue (trapq) class DumpTrapQ: def __init__(self, printer, name, trapq): self.printer = printer self.name = name self.trapq = trapq - self.last_api_msg = (0.0, 0.0) - self.api_dump = APIDumpHelper(printer, self._api_update) - wh = self.printer.lookup_object("webhooks") - wh.register_mux_endpoint( - "motion_report/dump_trapq", "name", name, self._add_api_client + self.last_batch_msg = (0.0, 0.0) + self.batch_bulk = bulk_sensor.BatchBulkHelper( + printer, self._process_batch + ) + api_resp = { + "header": ( + "time", + "duration", + "start_velocity", + "acceleration", + "start_position", + "direction", + ) + } + self.batch_bulk.add_mux_endpoint( + "motion_report/dump_trapq", "name", name, api_resp ) def extract_trapq(self, start_time, end_time): @@ -279,8 +178,8 @@ def get_trapq_position(self, print_time): velocity = move.start_v + move.accel * move_time return pos, velocity - def _api_update(self, eventtime): - qtime = self.last_api_msg[0] + min(self.last_api_msg[1], 0.100) + def _process_batch(self, eventtime): + qtime = self.last_batch_msg[0] + min(self.last_batch_msg[1], 0.100) data, cdata = self.extract_trapq(qtime, NEVER_TIME) d = [ ( @@ -293,25 +192,13 @@ def _api_update(self, eventtime): ) for m in data ] - if d and d[0] == self.last_api_msg: + if d and d[0] == self.last_batch_msg: d.pop(0) if not d: return {} - self.last_api_msg = d[-1] + self.last_batch_msg = d[-1] return {"data": d} - def _add_api_client(self, web_request): - self.api_dump.add_client(web_request) - hdr = ( - "time", - "duration", - "start_velocity", - "acceleration", - "start_position", - "direction", - ) - web_request.send({"header": hdr}) - STATUS_REFRESH_TIME = 0.250 diff --git a/klippy/extras/mpu9250.py b/klippy/extras/mpu9250.py index 9c67aa85a..292c1917a 100644 --- a/klippy/extras/mpu9250.py +++ b/klippy/extras/mpu9250.py @@ -4,11 +4,8 @@ # Copyright (C) 2020-2021 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. -import collections import logging -import threading -import time -from . import bus, motion_report, adxl345 +from . import bus, adxl345, bulk_sensor MPU9250_ADDR = 0x68 @@ -33,6 +30,7 @@ REG_USER_CTRL = 0x6A REG_PWR_MGMT_1 = 0x6B REG_PWR_MGMT_2 = 0x6C +REG_INT_STATUS = 0x3A SAMPLE_RATE_DIVS = {4000: 0x00} @@ -43,6 +41,10 @@ SET_PWR_MGMT_1_SLEEP = 0x40 SET_PWR_MGMT_2_ACCEL_ON = 0x07 SET_PWR_MGMT_2_OFF = 0x3F +SET_USER_FIFO_RESET = 0x04 +SET_USER_FIFO_EN = 0x40 +SET_ENABLE_FIFO = 0x08 +SET_DISABLE_FIFO = 0x00 FREEFALL_ACCEL = 9.80665 * 1000.0 # SCALE = 1/4096 g/LSB @8g scale * Earth gravity in mm/s**2 @@ -50,64 +52,49 @@ FIFO_SIZE = 512 -Accel_Measurement = collections.namedtuple( - "Accel_Measurement", ("time", "accel_x", "accel_y", "accel_z") -) +BYTES_PER_SAMPLE = 6 +SAMPLES_PER_BLOCK = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE -MIN_MSG_TIME = 0.100 +BATCH_UPDATES = 0.100 -BYTES_PER_SAMPLE = 6 -SAMPLES_PER_BLOCK = 8 # Printer class that controls MPU9250 chip class MPU9250: def __init__(self, config): self.printer = config.get_printer() adxl345.AccelCommandHelper(config, self) - self.query_rate = 0 - am = { - "x": (0, SCALE), - "y": (1, SCALE), - "z": (2, SCALE), - "-x": (0, -SCALE), - "-y": (1, -SCALE), - "-z": (2, -SCALE), - } - axes_map = config.getlist("axes_map", ("x", "y", "z"), count=3) - if any([a not in am for a in axes_map]): - raise config.error("Invalid mpu9250 axes_map parameter") - self.axes_map = [am[a.strip()] for a in axes_map] + self.axes_map = adxl345.read_axes_map(config) self.data_rate = config.getint("rate", 4000) if self.data_rate not in SAMPLE_RATE_DIVS: raise config.error("Invalid rate parameter: %d" % (self.data_rate,)) - # Measurement storage (accessed from background thread) - self.lock = threading.Lock() - self.raw_samples = [] # Setup mcu sensor_mpu9250 bulk query code self.i2c = bus.MCU_I2C_from_config( config, default_addr=MPU9250_ADDR, default_speed=400000 ) self.mcu = mcu = self.i2c.get_mcu() self.oid = oid = mcu.create_oid() - self.query_mpu9250_cmd = self.query_mpu9250_end_cmd = None - self.query_mpu9250_status_cmd = None + self.query_mpu9250_cmd = None mcu.register_config_callback(self._build_config) - mcu.register_response(self._handle_mpu9250_data, "mpu9250_data", oid) + self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=oid) # Clock tracking - self.last_sequence = self.max_query_duration = 0 - self.last_limit_count = self.last_error_count = 0 - self.clock_sync = adxl345.ClockSyncRegression(self.mcu, 640) - # API server endpoints - self.api_dump = motion_report.APIDumpHelper( - self.printer, self._api_update, self._api_startstop, 0.100 + chip_smooth = self.data_rate * BATCH_UPDATES * 2 + self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth) + self.clock_updater = bulk_sensor.ChipClockUpdater( + self.clock_sync, BYTES_PER_SAMPLE + ) + self.last_error_count = 0 + # Process messages in batches + self.batch_bulk = bulk_sensor.BatchBulkHelper( + self.printer, + self._process_batch, + self._start_measurements, + self._finish_measurements, + BATCH_UPDATES, ) self.name = config.get_name().split()[-1] - wh = self.printer.lookup_object("webhooks") - wh.register_mux_endpoint( - "mpu9250/dump_mpu9250", - "sensor", - self.name, - self._handle_dump_mpu9250, + hdr = ("time", "x_acceleration", "y_acceleration", "z_acceleration") + self.batch_bulk.add_mux_endpoint( + "mpu9250/dump_mpu9250", "sensor", self.name, {"header": hdr} ) def _build_config(self): @@ -116,25 +103,13 @@ def _build_config(self): "config_mpu9250 oid=%d i2c_oid=%d" % (self.oid, self.i2c.get_oid()) ) self.mcu.add_config_cmd( - "query_mpu9250 oid=%d clock=0 rest_ticks=0" % (self.oid,), - on_restart=True, + "query_mpu9250 oid=%d rest_ticks=0" % (self.oid,), on_restart=True ) self.query_mpu9250_cmd = self.mcu.lookup_command( - "query_mpu9250 oid=%c clock=%u rest_ticks=%u", cq=cmdqueue - ) - self.query_mpu9250_end_cmd = self.mcu.lookup_query_command( - "query_mpu9250 oid=%c clock=%u rest_ticks=%u", - "mpu9250_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%u limit_count=%hu", - oid=self.oid, - cq=cmdqueue, + "query_mpu9250 oid=%c rest_ticks=%u", cq=cmdqueue ) - self.query_mpu9250_status_cmd = self.mcu.lookup_query_command( - "query_mpu9250_status oid=%c", - "mpu9250_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%u limit_count=%hu", - oid=self.oid, - cq=cmdqueue, + self.clock_updater.setup_query_command( + self.mcu, "query_mpu9250_status oid=%c", oid=self.oid, cq=cmdqueue ) def read_reg(self, reg): @@ -144,18 +119,16 @@ def read_reg(self, reg): def set_reg(self, reg, val, minclock=0): self.i2c.i2c_write([reg, val & 0xFF], minclock=minclock) - # Measurement collection - def is_measuring(self): - return self.query_rate > 0 - - def _handle_mpu9250_data(self, params): - with self.lock: - self.raw_samples.append(params) + def start_internal_client(self): + aqh = adxl345.AccelQueryHelper(self.printer) + self.batch_bulk.add_client(aqh.handle_batch) + return aqh + # Measurement decoding def _extract_samples(self, raw_samples): # Load variables to optimize inner loop below (x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map - last_sequence = self.last_sequence + last_sequence = self.clock_updater.get_last_sequence() time_base, chip_base, inv_freq = self.clock_sync.get_time_translation() # Process every message in raw_samples count = seq = 0 @@ -186,45 +159,8 @@ def _extract_samples(self, raw_samples): del samples[count:] return samples - def _update_clock(self, minclock=0): - # Query current state - for retry in range(5): - params = self.query_mpu9250_status_cmd.send( - [self.oid], minclock=minclock - ) - fifo = params["fifo"] & 0x1FFF - if fifo <= FIFO_SIZE: - break - else: - raise self.printer.command_error("Unable to query mpu9250 fifo") - mcu_clock = self.mcu.clock32_to_clock64(params["clock"]) - seq_diff = (params["next_sequence"] - self.last_sequence) & 0xFFFF - self.last_sequence += seq_diff - buffered = params["buffered"] - lc_diff = (params["limit_count"] - self.last_limit_count) & 0xFFFF - self.last_limit_count += lc_diff - duration = params["query_ticks"] - if duration > self.max_query_duration: - # Skip measurement as a high query time could skew clock tracking - self.max_query_duration = max( - 2 * self.max_query_duration, self.mcu.seconds_to_clock(0.000005) - ) - return - self.max_query_duration = 2 * duration - msg_count = ( - self.last_sequence * SAMPLES_PER_BLOCK - + buffered // BYTES_PER_SAMPLE - + fifo - ) - # The "chip clock" is the message counter plus .5 for average - # inaccuracy of query responses and plus .5 for assumed offset - # of mpu9250 hw processing time. - chip_clock = msg_count + 1 - self.clock_sync.update(mcu_clock + duration // 2, chip_clock) - + # Start, stop, and process message batches def _start_measurements(self): - if self.is_measuring(): - return # In case of miswiring, testing MPU9250 device ID prevents treating # noise or wrong signal as a correctly initialized device dev_id = self.read_reg(REG_DEVID) @@ -239,51 +175,46 @@ def _start_measurements(self): # Setup chip in requested query rate self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_WAKE) self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_ACCEL_ON) - time.sleep(20.0 / 1000) # wait for accelerometer chip wake up - self.set_reg(REG_SMPLRT_DIV, SAMPLE_RATE_DIVS[self.data_rate]) + # Add 20ms pause for accelerometer chip wake up + self.read_reg(REG_DEVID) # Dummy read to ensure queues flushed + systime = self.printer.get_reactor().monotonic() + next_time = self.mcu.estimated_print_time(systime) + 0.020 + self.set_reg( + REG_SMPLRT_DIV, + SAMPLE_RATE_DIVS[self.data_rate], + minclock=self.mcu.print_time_to_clock(next_time), + ) self.set_reg(REG_CONFIG, SET_CONFIG) self.set_reg(REG_ACCEL_CONFIG, SET_ACCEL_CONFIG) self.set_reg(REG_ACCEL_CONFIG2, SET_ACCEL_CONFIG2) + # Reset fifo + self.set_reg(REG_FIFO_EN, SET_DISABLE_FIFO) + self.set_reg(REG_USER_CTRL, SET_USER_FIFO_RESET) + self.set_reg(REG_USER_CTRL, SET_USER_FIFO_EN) + self.read_reg(REG_INT_STATUS) # clear FIFO overflow flag - # Setup samples - with self.lock: - self.raw_samples = [] # Start bulk reading - systime = self.printer.get_reactor().monotonic() - print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME - reqclock = self.mcu.print_time_to_clock(print_time) + self.bulk_queue.clear_samples() rest_ticks = self.mcu.seconds_to_clock(4.0 / self.data_rate) - self.query_rate = self.data_rate - self.query_mpu9250_cmd.send( - [self.oid, reqclock, rest_ticks], reqclock=reqclock - ) + self.query_mpu9250_cmd.send([self.oid, rest_ticks]) + self.set_reg(REG_FIFO_EN, SET_ENABLE_FIFO) logging.info("MPU9250 starting '%s' measurements", self.name) # Initialize clock tracking - self.last_sequence = 0 - self.last_limit_count = self.last_error_count = 0 - self.clock_sync.reset(reqclock, 0) - self.max_query_duration = 1 << 31 - self._update_clock(minclock=reqclock) - self.max_query_duration = 1 << 31 + self.clock_updater.note_start() + self.last_error_count = 0 def _finish_measurements(self): - if not self.is_measuring(): - return # Halt bulk reading - params = self.query_mpu9250_end_cmd.send([self.oid, 0, 0]) - self.query_rate = 0 - with self.lock: - self.raw_samples = [] + self.set_reg(REG_FIFO_EN, SET_DISABLE_FIFO) + self.query_mpu9250_cmd.send_wait_ack([self.oid, 0]) + self.bulk_queue.clear_samples() logging.info("MPU9250 finished '%s' measurements", self.name) self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_SLEEP) self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_OFF) - # API interface - def _api_update(self, eventtime): - self._update_clock() - with self.lock: - raw_samples = self.raw_samples - self.raw_samples = [] + def _process_batch(self, eventtime): + self.clock_updater.update_clock() + raw_samples = self.bulk_queue.pull_samples() if not raw_samples: return {} samples = self._extract_samples(raw_samples) @@ -292,24 +223,9 @@ def _api_update(self, eventtime): return { "data": samples, "errors": self.last_error_count, - "overflows": self.last_limit_count, + "overflows": self.clock_updater.get_last_overflows(), } - def _api_startstop(self, is_start): - if is_start: - self._start_measurements() - else: - self._finish_measurements() - - def _handle_dump_mpu9250(self, web_request): - self.api_dump.add_client(web_request) - hdr = ("time", "x_acceleration", "y_acceleration", "z_acceleration") - web_request.send({"header": hdr}) - - def start_internal_client(self): - cconn = self.api_dump.add_internal_client() - return adxl345.AccelQueryHelper(self.printer, cconn) - def load_config(config): return MPU9250(config) diff --git a/klippy/extras/multi_pin.py b/klippy/extras/multi_pin.py index c5d41d32d..36a02b43f 100644 --- a/klippy/extras/multi_pin.py +++ b/klippy/extras/multi_pin.py @@ -56,9 +56,9 @@ def set_digital(self, print_time, value): for mcu_pin in self.mcu_pins: mcu_pin.set_digital(print_time, value) - def set_pwm(self, print_time, value, cycle_time=None): + def set_pwm(self, print_time, value): for mcu_pin in self.mcu_pins: - mcu_pin.set_pwm(print_time, value, cycle_time) + mcu_pin.set_pwm(print_time, value) def load_config_prefix(config): diff --git a/klippy/extras/output_pin.py b/klippy/extras/output_pin.py index 95778e327..158628060 100644 --- a/klippy/extras/output_pin.py +++ b/klippy/extras/output_pin.py @@ -1,6 +1,6 @@ -# Code to configure miscellaneous chips +# PWM and digital output pin handling # -# Copyright (C) 2017-2021 Kevin O'Connor +# Copyright (C) 2017-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. @@ -13,6 +13,7 @@ class PrinterOutputPin: def __init__(self, config): self.printer = config.get_printer() ppins = self.printer.lookup_object("pins") + # Determine pin type self.is_pwm = config.getboolean("pwm", False) if self.is_pwm: self.mcu_pin = ppins.setup_pin("pwm", config.get("pin")) @@ -22,35 +23,29 @@ def __init__(self, config): hardware_pwm = config.getboolean("hardware_pwm", False) self.mcu_pin.setup_cycle_time(cycle_time, hardware_pwm) self.scale = config.getfloat("scale", 1.0, above=0.0) - self.last_cycle_time = self.default_cycle_time = cycle_time else: self.mcu_pin = ppins.setup_pin("digital_out", config.get("pin")) self.scale = 1.0 - self.last_cycle_time = self.default_cycle_time = 0.0 self.last_print_time = 0.0 - static_value = config.getfloat( - "static_value", None, minval=0.0, maxval=self.scale - ) + # Support mcu checking for maximum duration self.reactor = self.printer.get_reactor() self.resend_timer = None self.resend_interval = 0.0 + max_mcu_duration = config.getfloat( + "maximum_mcu_duration", 0.0, minval=0.500, maxval=MAX_SCHEDULE_TIME + ) + self.mcu_pin.setup_max_duration(max_mcu_duration) + if max_mcu_duration: + config.deprecate("maximum_mcu_duration") + self.resend_interval = max_mcu_duration - RESEND_HOST_TIME + # Determine start and shutdown values + static_value = config.getfloat( + "static_value", None, minval=0.0, maxval=self.scale + ) if static_value is not None: - self.mcu_pin.setup_max_duration(0.0) - self.last_value = static_value / self.scale - self.mcu_pin.setup_start_value( - self.last_value, self.last_value, True - ) + config.deprecate("static_value") + self.last_value = self.shutdown_value = static_value / self.scale else: - max_mcu_duration = config.getfloat( - "maximum_mcu_duration", - 0.0, - minval=0.500, - maxval=MAX_SCHEDULE_TIME, - ) - self.mcu_pin.setup_max_duration(max_mcu_duration) - if max_mcu_duration: - self.resend_interval = max_mcu_duration - RESEND_HOST_TIME - self.last_value = ( config.getfloat("value", 0.0, minval=0.0, maxval=self.scale) / self.scale @@ -61,31 +56,30 @@ def __init__(self, config): ) / self.scale ) - self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value) - pin_name = config.get_name().split()[1] - gcode = self.printer.lookup_object("gcode") - gcode.register_mux_command( - "SET_PIN", - "PIN", - pin_name, - self.cmd_SET_PIN, - desc=self.cmd_SET_PIN_help, - ) + self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value) + # Register commands + pin_name = config.get_name().split()[1] + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command( + "SET_PIN", + "PIN", + pin_name, + self.cmd_SET_PIN, + desc=self.cmd_SET_PIN_help, + ) def get_status(self, eventtime): return {"value": self.last_value} - def _set_pin(self, print_time, value, cycle_time, is_resend=False): - if value == self.last_value and cycle_time == self.last_cycle_time: - if not is_resend: - return + def _set_pin(self, print_time, value, is_resend=False): + if value == self.last_value and not is_resend: + return print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) if self.is_pwm: - self.mcu_pin.set_pwm(print_time, value, cycle_time) + self.mcu_pin.set_pwm(print_time, value) else: self.mcu_pin.set_digital(print_time, value) self.last_value = value - self.last_cycle_time = cycle_time self.last_print_time = print_time if self.resend_interval and self.resend_timer is None: self.resend_timer = self.reactor.register_timer( @@ -95,19 +89,15 @@ def _set_pin(self, print_time, value, cycle_time, is_resend=False): cmd_SET_PIN_help = "Set the value of an output pin" def cmd_SET_PIN(self, gcmd): + # Read requested value value = gcmd.get_float("VALUE", minval=0.0, maxval=self.scale) value /= self.scale - cycle_time = gcmd.get_float( - "CYCLE_TIME", - self.default_cycle_time, - above=0.0, - maxval=MAX_SCHEDULE_TIME, - ) if not self.is_pwm and value not in [0.0, 1.0]: raise gcmd.error("Invalid pin value") + # Obtain print_time and apply requested settings toolhead = self.printer.lookup_object("toolhead") toolhead.register_lookahead_callback( - lambda print_time: self._set_pin(print_time, value, cycle_time) + lambda print_time: self._set_pin(print_time, value) ) def _resend_current_val(self, eventtime): @@ -122,12 +112,7 @@ def _resend_current_val(self, eventtime): if time_diff > 0.0: # Reschedule for resend time return systime + time_diff - self._set_pin( - print_time + PIN_MIN_TIME, - self.last_value, - self.last_cycle_time, - True, - ) + self._set_pin(print_time + PIN_MIN_TIME, self.last_value, True) return systime + self.resend_interval diff --git a/klippy/extras/pwm_cycle_time.py b/klippy/extras/pwm_cycle_time.py new file mode 100644 index 000000000..497ddaec2 --- /dev/null +++ b/klippy/extras/pwm_cycle_time.py @@ -0,0 +1,165 @@ +# Handle pwm output pins with variable frequency +# +# Copyright (C) 2017-2023 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +PIN_MIN_TIME = 0.100 +MAX_SCHEDULE_TIME = 5.0 + + +class MCU_pwm_cycle: + def __init__(self, pin_params, cycle_time, start_value, shutdown_value): + self._mcu = pin_params["chip"] + self._cycle_time = cycle_time + self._oid = None + self._mcu.register_config_callback(self._build_config) + self._pin = pin_params["pin"] + self._invert = pin_params["invert"] + if self._invert: + start_value = 1.0 - start_value + shutdown_value = 1.0 - shutdown_value + self._start_value = max(0.0, min(1.0, start_value)) + self._shutdown_value = max(0.0, min(1.0, shutdown_value)) + self._last_clock = self._cycle_ticks = 0 + self._set_cmd = self._set_cycle_ticks = None + + def _build_config(self): + cmd_queue = self._mcu.alloc_command_queue() + curtime = self._mcu.get_printer().get_reactor().monotonic() + printtime = self._mcu.estimated_print_time(curtime) + self._last_clock = self._mcu.print_time_to_clock(printtime + 0.200) + cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) + if self._shutdown_value not in [0.0, 1.0]: + raise self._mcu.get_printer().config_error( + "shutdown value must be 0.0 or 1.0 on soft pwm" + ) + if cycle_ticks >= 1 << 31: + raise self._mcu.get_printer().config_error( + "PWM pin cycle time too large" + ) + self._mcu.request_move_queue_slot() + self._oid = self._mcu.create_oid() + self._mcu.add_config_cmd( + "config_digital_out oid=%d pin=%s value=%d" + " default_value=%d max_duration=%d" + % ( + self._oid, + self._pin, + self._start_value >= 1.0, + self._shutdown_value >= 0.5, + 0, + ) + ) + self._mcu.add_config_cmd( + "set_digital_out_pwm_cycle oid=%d cycle_ticks=%d" + % (self._oid, cycle_ticks) + ) + self._cycle_ticks = cycle_ticks + svalue = int(self._start_value * cycle_ticks + 0.5) + self._mcu.add_config_cmd( + "queue_digital_out oid=%d clock=%d on_ticks=%d" + % (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 + ) + self._set_cycle_ticks = self._mcu.lookup_command( + "set_digital_out_pwm_cycle oid=%c cycle_ticks=%u", cq=cmd_queue + ) + + def set_pwm_cycle(self, print_time, value, cycle_time): + clock = self._mcu.print_time_to_clock(print_time) + minclock = self._last_clock + # Send updated cycle_time if necessary + cycle_ticks = self._mcu.seconds_to_clock(cycle_time) + if cycle_ticks != self._cycle_ticks: + if cycle_ticks >= 1 << 31: + raise self._mcu.get_printer().command_error( + "PWM cycle time too large" + ) + self._set_cycle_ticks.send( + [self._oid, cycle_ticks], minclock=minclock, reqclock=clock + ) + self._cycle_ticks = cycle_ticks + # Send pwm update + if self._invert: + value = 1.0 - value + v = int(max(0.0, min(1.0, value)) * float(self._cycle_ticks) + 0.5) + self._set_cmd.send( + [self._oid, clock, v], minclock=self._last_clock, reqclock=clock + ) + self._last_clock = clock + + +class PrinterOutputPWMCycle: + def __init__(self, config): + self.printer = config.get_printer() + self.last_print_time = 0.0 + cycle_time = config.getfloat( + "cycle_time", 0.100, above=0.0, maxval=MAX_SCHEDULE_TIME + ) + self.last_cycle_time = self.default_cycle_time = cycle_time + # Determine start and shutdown values + self.scale = config.getfloat("scale", 1.0, above=0.0) + self.last_value = ( + config.getfloat("value", 0.0, minval=0.0, maxval=self.scale) + / self.scale + ) + self.shutdown_value = ( + config.getfloat( + "shutdown_value", 0.0, minval=0.0, maxval=self.scale + ) + / self.scale + ) + # Create pwm pin object + ppins = self.printer.lookup_object("pins") + pin_params = ppins.lookup_pin(config.get("pin"), can_invert=True) + self.mcu_pin = MCU_pwm_cycle( + pin_params, cycle_time, self.last_value, self.shutdown_value + ) + # Register commands + pin_name = config.get_name().split()[1] + gcode = self.printer.lookup_object("gcode") + gcode.register_mux_command( + "SET_PIN", + "PIN", + pin_name, + self.cmd_SET_PIN, + desc=self.cmd_SET_PIN_help, + ) + + def get_status(self, eventtime): + return {"value": self.last_value} + + def _set_pin(self, print_time, value, cycle_time): + if value == self.last_value and cycle_time == self.last_cycle_time: + return + print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) + self.mcu_pin.set_pwm_cycle(print_time, value, cycle_time) + self.last_value = value + self.last_cycle_time = cycle_time + self.last_print_time = print_time + + cmd_SET_PIN_help = "Set the value of an output pin" + + def cmd_SET_PIN(self, gcmd): + # Read requested value + value = gcmd.get_float("VALUE", minval=0.0, maxval=self.scale) + value /= self.scale + cycle_time = gcmd.get_float( + "CYCLE_TIME", + self.default_cycle_time, + above=0.0, + maxval=MAX_SCHEDULE_TIME, + ) + # Obtain print_time and apply requested settings + toolhead = self.printer.lookup_object("toolhead") + toolhead.register_lookahead_callback( + lambda print_time: self._set_pin(print_time, value, cycle_time) + ) + + +def load_config_prefix(config): + return PrinterOutputPWMCycle(config) diff --git a/klippy/extras/pwm_tool.py b/klippy/extras/pwm_tool.py index d89293a51..c9eb69d48 100644 --- a/klippy/extras/pwm_tool.py +++ b/klippy/extras/pwm_tool.py @@ -6,7 +6,6 @@ import chelper MAX_SCHEDULE_TIME = 5.0 -CLOCK_SYNC_EXTRA_TIME = 0.050 class error(Exception): @@ -147,9 +146,7 @@ def _send_update(self, clock, val): # Continue flushing to resend time wakeclock += self._duration_ticks wake_print_time = self._mcu.clock_to_print_time(wakeclock) - self._toolhead.note_kinematic_activity( - wake_print_time + CLOCK_SYNC_EXTRA_TIME - ) + self._toolhead.note_mcu_movequeue_activity(wake_print_time) def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) diff --git a/klippy/extras/replicape.py b/klippy/extras/replicape.py index 41131abd0..0e6d14f0b 100644 --- a/klippy/extras/replicape.py +++ b/klippy/extras/replicape.py @@ -31,7 +31,6 @@ def __init__(self, replicape, channel, pin_type, pin_params): self._invert = pin_params["invert"] self._start_value = self._shutdown_value = float(self._invert) self._is_enable = not not self._start_value - self._is_static = False self._last_clock = 0 self._pwm_max = 0.0 self._set_cmd = None @@ -52,15 +51,12 @@ def setup_cycle_time(self, cycle_time, hardware_pwm=False): self._cycle_time, ) - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): if self._invert: start_value = 1.0 - start_value shutdown_value = 1.0 - shutdown_value self._start_value = max(0.0, min(1.0, start_value)) self._shutdown_value = max(0.0, min(1.0, shutdown_value)) - self._is_static = is_static self._replicape.note_pwm_start_value( self._channel, self._start_value, self._shutdown_value ) @@ -69,19 +65,6 @@ def setup_start_value(self, start_value, shutdown_value, is_static=False): def _build_config(self): self._pwm_max = self._mcu.get_constant_float("PCA9685_MAX") cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time) - if self._is_static: - self._mcu.add_config_cmd( - "set_pca9685_out bus=%d addr=%d channel=%d" - " cycle_ticks=%d value=%d" - % ( - self._bus, - self._address, - self._channel, - cycle_ticks, - self._start_value * self._pwm_max, - ) - ) - return self._mcu.request_move_queue_slot() self._oid = self._mcu.create_oid() self._mcu.add_config_cmd( @@ -103,7 +86,7 @@ def _build_config(self): "queue_pca9685_out oid=%c clock=%u value=%hu", cq=cmd_queue ) - def set_pwm(self, print_time, value, cycle_time=None): + def set_pwm(self, print_time, value): clock = self._mcu.print_time_to_clock(print_time) if self._invert: value = 1.0 - value diff --git a/klippy/extras/static_digital_output.py b/klippy/extras/static_digital_output.py index bc8be101a..8c39fb3c7 100644 --- a/klippy/extras/static_digital_output.py +++ b/klippy/extras/static_digital_output.py @@ -11,8 +11,12 @@ def __init__(self, config): ppins = printer.lookup_object("pins") pin_list = config.getlist("pins") for pin_desc in pin_list: - mcu_pin = ppins.setup_pin("digital_out", pin_desc) - mcu_pin.setup_start_value(1, 1, True) + pin_params = ppins.lookup_pin(pin_desc, can_invert=True) + mcu = pin_params["chip"] + mcu.add_config_cmd( + "set_digital_out pin=%s value=%d" + % (pin_params["pin"], not pin_params["invert"]) + ) def load_config_prefix(config): diff --git a/klippy/extras/sx1509.py b/klippy/extras/sx1509.py index 00c630bb7..f82b73282 100644 --- a/klippy/extras/sx1509.py +++ b/klippy/extras/sx1509.py @@ -146,7 +146,6 @@ def __init__(self, sx1509, pin_params): self._invert = pin_params["invert"] self._mcu.register_config_callback(self._build_config) self._start_value = self._shutdown_value = self._invert - self._is_static = False self._max_duration = 2.0 self._set_cmd = self._clear_cmd = None # Set direction to output @@ -162,12 +161,9 @@ def get_mcu(self): def setup_max_duration(self, max_duration): self._max_duration = max_duration - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): self._start_value = (not not start_value) ^ self._invert self._shutdown_value = self._invert - self._is_static = is_static # We need to set the start value here so the register is # updated before the SX1509 class writes it. if self._start_value: @@ -197,7 +193,6 @@ def __init__(self, sx1509, pin_params): self._invert = pin_params["invert"] self._mcu.register_config_callback(self._build_config) self._start_value = self._shutdown_value = float(self._invert) - self._is_static = False self._max_duration = 2.0 self._hardware_pwm = False self._pwm_max = 0.0 @@ -241,17 +236,14 @@ def setup_cycle_time(self, cycle_time, hardware_pwm=False): self._cycle_time = cycle_time self._hardware_pwm = hardware_pwm - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): if self._invert: start_value = 1.0 - start_value shutdown_value = 1.0 - shutdown_value self._start_value = max(0.0, min(1.0, start_value)) self._shutdown_value = max(0.0, min(1.0, shutdown_value)) - self._is_static = is_static - def set_pwm(self, print_time, value, cycle_time=None): + def set_pwm(self, print_time, value): self._sx1509.set_register( self._i_on_reg, ~int(255 * value) if not self._invert else int(255 * value) & 0xFF, diff --git a/klippy/extras/virtual_sdcard.py b/klippy/extras/virtual_sdcard.py index d61ea3d70..f075219cd 100644 --- a/klippy/extras/virtual_sdcard.py +++ b/klippy/extras/virtual_sdcard.py @@ -305,7 +305,7 @@ def work_handler(self, eventtime): # Dispatch command self.cmd_from_sd = True line = lines.pop() - next_file_position = self.file_position + len(line) + 1 + next_file_position = self.file_position + len(line.encode()) + 1 self.next_file_position = next_file_position try: self.gcode.run_script(line) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index 35a2a8723..5c8428c0d 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -286,8 +286,8 @@ def __init__(self, config, extruder_num): desc=self.cmd_ACTIVATE_EXTRUDER_help, ) - def update_move_time(self, flush_time): - self.trapq_finalize_moves(self.trapq, flush_time) + def update_move_time(self, flush_time, clear_history_time): + self.trapq_finalize_moves(self.trapq, flush_time, clear_history_time) def get_status(self, eventtime): sts = self.heater.get_status(eventtime) @@ -424,7 +424,7 @@ class DummyExtruder: def __init__(self, printer): self.printer = printer - def update_move_time(self, flush_time): + def update_move_time(self, flush_time, clear_history_time): pass def check_move(self, move): diff --git a/klippy/mcu.py b/klippy/mcu.py index 9c43d7a9c..16cc33061 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -116,6 +116,10 @@ def send(self, data=(), minclock=0, reqclock=0): cmd = self._cmd.encode(data) self._serial.raw_send(cmd, minclock, reqclock, self._cmd_queue) + def send_wait_ack(self, data=(), minclock=0, reqclock=0): + cmd = self._cmd.encode(data) + self._serial.raw_send_wait_ack(cmd, minclock, reqclock, self._cmd_queue) + def get_command_tag(self): return self._msgtag @@ -241,16 +245,17 @@ def _handle_trsync_state(self, params): [self._oid, self.REASON_PAST_END_TIME] ) - def start(self, print_time, trigger_completion, expire_timeout): + def start( + self, print_time, report_offset, trigger_completion, expire_timeout + ): self._trigger_completion = trigger_completion self._home_end_clock = None clock = self._mcu.print_time_to_clock(print_time) expire_ticks = self._mcu.seconds_to_clock(expire_timeout) expire_clock = clock + expire_ticks - report_ticks = self._mcu.seconds_to_clock(expire_timeout * 0.4) - min_extend_ticks = self._mcu.seconds_to_clock( - expire_timeout * 0.4 * 0.8 - ) + report_ticks = self._mcu.seconds_to_clock(expire_timeout * 0.3) + report_clock = clock + int(report_ticks * report_offset + 0.5) + min_extend_ticks = int(report_ticks * 0.8 + 0.5) ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.trdispatch_mcu_setup( self._trdispatch_mcu, @@ -263,8 +268,8 @@ def start(self, print_time, trigger_completion, expire_timeout): self._handle_trsync_state, "trsync_state", self._oid ) self._trsync_start_cmd.send( - [self._oid, clock, report_ticks, self.REASON_COMMS_TIMEOUT], - reqclock=clock, + [self._oid, report_clock, report_ticks, self.REASON_COMMS_TIMEOUT], + reqclock=report_clock, ) for s in self._steppers: self._stepper_stop_cmd.send([s.get_oid(), self._oid]) @@ -375,8 +380,14 @@ def home_start( expire_timeout = self.danger_options.multi_mcu_trsync_timeout if len(self._trsyncs) == 1: expire_timeout = TRSYNC_SINGLE_MCU_TIMEOUT - for trsync in self._trsyncs: - trsync.start(print_time, self._trigger_completion, expire_timeout) + for i, trsync in enumerate(self._trsyncs): + report_offset = float(i) / len(self._trsyncs) + trsync.start( + print_time, + report_offset, + self._trigger_completion, + expire_timeout, + ) etrsync = self._trsyncs[0] ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.trdispatch_start(self._trdispatch, etrsync.REASON_HOST_REQUEST) @@ -431,7 +442,6 @@ def __init__(self, mcu, pin_params): self._pin = pin_params["pin"] self._invert = pin_params["invert"] self._start_value = self._shutdown_value = self._invert - self._is_static = False self._max_duration = 2.0 self._last_clock = 0 self._set_cmd = None @@ -442,20 +452,11 @@ def get_mcu(self): def setup_max_duration(self, max_duration): self._max_duration = max_duration - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): self._start_value = (not not start_value) ^ self._invert self._shutdown_value = (not not shutdown_value) ^ self._invert - self._is_static = is_static def _build_config(self): - if self._is_static: - self._mcu.add_config_cmd( - "set_digital_out pin=%s value=%d" - % (self._pin, self._start_value) - ) - return if self._max_duration and self._start_value != self._shutdown_value: raise pins.error( "Pin with max duration must have start" @@ -508,10 +509,9 @@ def __init__(self, mcu, pin_params): self._pin = pin_params["pin"] self._invert = pin_params["invert"] self._start_value = self._shutdown_value = float(self._invert) - self._is_static = False - self._last_clock = self._last_cycle_ticks = 0 + self._last_clock = 0 self._pwm_max = 0.0 - self._set_cmd = self._set_cycle_ticks = None + self._set_cmd = None def get_mcu(self): return self._mcu @@ -523,15 +523,12 @@ def setup_cycle_time(self, cycle_time, hardware_pwm=False): self._cycle_time = cycle_time self._hardware_pwm = hardware_pwm - def setup_start_value(self, start_value, shutdown_value, is_static=False): - if is_static and start_value != shutdown_value: - raise pins.error("Static pin can not have shutdown value") + def setup_start_value(self, start_value, shutdown_value): if self._invert: start_value = 1.0 - start_value shutdown_value = 1.0 - shutdown_value self._start_value = max(0.0, min(1.0, start_value)) self._shutdown_value = max(0.0, min(1.0, shutdown_value)) - self._is_static = is_static def _build_config(self): if self._max_duration and self._start_value != self._shutdown_value: @@ -549,16 +546,6 @@ def _build_config(self): raise pins.error("PWM pin max duration too large") if self._hardware_pwm: self._pwm_max = self._mcu.get_constant_float("PWM_MAX") - if self._is_static: - self._mcu.add_config_cmd( - "set_pwm_out pin=%s cycle_ticks=%d value=%d" - % ( - self._pin, - cycle_ticks, - self._start_value * self._pwm_max, - ) - ) - return self._mcu.request_move_queue_slot() self._oid = self._mcu.create_oid() self._mcu.add_config_cmd( @@ -586,12 +573,6 @@ def _build_config(self): # Software PWM if self._shutdown_value not in [0.0, 1.0]: raise pins.error("shutdown value must be 0.0 or 1.0 on soft pwm") - if self._is_static: - self._mcu.add_config_cmd( - "set_digital_out pin=%s value=%d" - % (self._pin, self._start_value >= 0.5) - ) - return if cycle_ticks >= 1 << 31: raise pins.error("PWM pin cycle time too large") self._mcu.request_move_queue_slot() @@ -611,7 +592,7 @@ def _build_config(self): "set_digital_out_pwm_cycle oid=%d cycle_ticks=%d" % (self._oid, cycle_ticks) ) - self._last_cycle_ticks = cycle_ticks + self._pwm_max = float(cycle_ticks) svalue = int(self._start_value * cycle_ticks + 0.5) self._mcu.add_config_cmd( "queue_digital_out oid=%d clock=%d on_ticks=%d" @@ -621,39 +602,16 @@ def _build_config(self): self._set_cmd = self._mcu.lookup_command( "queue_digital_out oid=%c clock=%u on_ticks=%u", cq=cmd_queue ) - self._set_cycle_ticks = self._mcu.lookup_command( - "set_digital_out_pwm_cycle oid=%c cycle_ticks=%u", cq=cmd_queue - ) - def set_pwm(self, print_time, value, cycle_time=None): - clock = self._mcu.print_time_to_clock(print_time) - minclock = self._last_clock - self._last_clock = clock + def set_pwm(self, print_time, value): if self._invert: value = 1.0 - value - if self._hardware_pwm: - v = int(max(0.0, min(1.0, value)) * self._pwm_max + 0.5) - self._set_cmd.send( - [self._oid, clock, v], minclock=minclock, reqclock=clock - ) - return - # Soft pwm update - if cycle_time is None: - cycle_time = self._cycle_time - cycle_ticks = self._mcu.seconds_to_clock(cycle_time) - if cycle_ticks != self._last_cycle_ticks: - if cycle_ticks >= 1 << 31: - raise self._mcu.get_printer().command_error( - "PWM cycle time too large" - ) - self._set_cycle_ticks.send( - [self._oid, cycle_ticks], minclock=minclock, reqclock=clock - ) - self._last_cycle_ticks = cycle_ticks - on_ticks = int(max(0.0, min(1.0, value)) * float(cycle_ticks) + 0.5) + v = int(max(0.0, min(1.0, value)) * self._pwm_max + 0.5) + clock = self._mcu.print_time_to_clock(print_time) self._set_cmd.send( - [self._oid, clock, on_ticks], minclock=minclock, reqclock=clock + [self._oid, clock, v], minclock=self._last_clock, reqclock=clock ) + self._last_clock = clock class MCU_adc: @@ -1243,7 +1201,7 @@ def request_move_queue_slot(self): def register_flush_callback(self, callback): self._flush_callbacks.append(callback) - def flush_moves(self, print_time): + def flush_moves(self, print_time, clear_history_time): if self._steppersync is None: return clock = self.print_time_to_clock(print_time) @@ -1251,7 +1209,12 @@ def flush_moves(self, print_time): return for cb in self._flush_callbacks: cb(print_time, clock) - ret = self._ffi_lib.steppersync_flush(self._steppersync, clock) + clear_history_clock = max( + 0, self.print_time_to_clock(clear_history_time) + ) + ret = self._ffi_lib.steppersync_flush( + self._steppersync, clock, clear_history_clock + ) if ret: raise error( "Internal error in MCU '%s' stepcompress" % (self._name,) diff --git a/klippy/stepper.py b/klippy/stepper.py index 0a79f88ea..0117d0e39 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -400,10 +400,10 @@ def __init__( self.endstop_map = {} self.add_extra_stepper(config) mcu_stepper = self.steppers[0] + self._tmc_current_helpers = None self.get_name = mcu_stepper.get_name self.get_commanded_position = mcu_stepper.get_commanded_position self.calc_position_from_coord = mcu_stepper.calc_position_from_coord - self.get_tmc_current_helper = mcu_stepper.get_tmc_current_helper # Primary endstop position mcu_endstop = self.endstops[0][0] if hasattr(mcu_endstop, "get_position_endstop"): @@ -482,6 +482,13 @@ def __init__( % (config.get_name(),) ) + def get_tmc_current_helpers(self): + if self._tmc_current_helpers is None: + self._tmc_current_helpers = [ + s.get_tmc_current_helper() for s in self.steppers + ] + return self._tmc_current_helpers + def get_range(self): return self.position_min, self.position_max diff --git a/klippy/toolhead.py b/klippy/toolhead.py index d38eda6fa..fac938da3 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -1,6 +1,6 @@ # Code for coordinating events on the printer toolhead # -# Copyright (C) 2016-2021 Kevin O'Connor +# Copyright (C) 2016-2024 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. import math, logging, importlib @@ -132,7 +132,7 @@ def set_junction(self, start_v2, cruise_v2, end_v2): # Class to track a list of pending move requests and to facilitate # "look-ahead" across moves to reduce acceleration between moves. -class MoveQueue: +class LookAheadQueue: def __init__(self, toolhead): self.toolhead = toolhead self.queue = [] @@ -230,10 +230,12 @@ def add_move(self, move): BUFFER_TIME_START = 0.250 BGFLUSH_LOW_TIME = 0.200 BGFLUSH_BATCH_TIME = 0.200 +BGFLUSH_EXTRA_TIME = 0.250 MIN_KIN_TIME = 0.100 MOVE_BATCH_TIME = 0.500 STEPCOMPRESS_FLUSH_TIME = 0.050 SDS_CHECK_TIME = 0.001 # step+dir+step filter in stepcompress.c +MOVE_HISTORY_EXPIRE = 30.0 DRIP_SEGMENT_TIME = 0.050 DRIP_TIME = 0.100 @@ -252,8 +254,8 @@ def __init__(self, config): m for n, m in self.printer.lookup_objects(module="mcu") ] self.mcu = self.all_mcus[0] - self.move_queue = MoveQueue(self) - self.move_queue.set_flush_time(BUFFER_TIME_HIGH) + self.lookahead = LookAheadQueue(self) + self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.commanded_pos = [0.0, 0.0, 0.0, 0.0] # Velocity and acceleration control self.max_velocity = config.getfloat("max_velocity", above=0.0) @@ -283,7 +285,10 @@ def __init__(self, config): # Flush tracking self.flush_timer = self.reactor.register_timer(self._flush_handler) self.do_kick_flush_timer = True - self.last_flush_time = self.need_flush_time = self.step_gen_time = 0.0 + self.last_flush_time = self.min_restart_time = 0.0 + self.need_flush_time = ( + self.step_gen_time + ) = self.clear_history_time = 0.0 # Kinematic step generation scan window time tracking self.kin_flush_delay = SDS_CHECK_TIME self.kin_flush_times = [] @@ -348,17 +353,24 @@ def get_active_rails_for_axis(self, axis): def _advance_flush_time(self, flush_time): flush_time = max(flush_time, self.last_flush_time) # Generate steps via itersolve - sg_flush_ceil = max(flush_time, self.print_time - self.kin_flush_delay) - sg_flush_time = min(flush_time + STEPCOMPRESS_FLUSH_TIME, sg_flush_ceil) + sg_flush_want = min( + flush_time + STEPCOMPRESS_FLUSH_TIME, + self.print_time - self.kin_flush_delay, + ) + sg_flush_time = max(sg_flush_want, flush_time) for sg in self.step_generators: sg(sg_flush_time) + self.min_restart_time = max(self.min_restart_time, sg_flush_time) # Free trapq entries that are no longer needed + clear_history_time = self.clear_history_time + if not self.can_pause: + clear_history_time = flush_time - MOVE_HISTORY_EXPIRE free_time = sg_flush_time - self.kin_flush_delay - self.trapq_finalize_moves(self.trapq, free_time) - self.extruder.update_move_time(free_time) + self.trapq_finalize_moves(self.trapq, free_time, clear_history_time) + self.extruder.update_move_time(free_time, clear_history_time) # Flush stepcompress and mcu steppersync for m in self.all_mcus: - m.flush_moves(flush_time) + m.flush_moves(flush_time, clear_history_time) self.last_flush_time = flush_time def _advance_move_time(self, next_print_time): @@ -375,7 +387,7 @@ def _advance_move_time(self, next_print_time): def _calc_print_time(self): curtime = self.reactor.monotonic() est_print_time = self.mcu.estimated_print_time(curtime) - kin_time = max(est_print_time + MIN_KIN_TIME, self.last_flush_time) + kin_time = max(est_print_time + MIN_KIN_TIME, self.min_restart_time) kin_time += self.kin_flush_delay min_print_time = max(est_print_time + BUFFER_TIME_START, kin_time) if min_print_time > self.print_time: @@ -425,29 +437,30 @@ def _process_moves(self, moves): # Generate steps for moves if self.special_queuing_state: self._update_drip_move_time(next_move_time) - self.note_kinematic_activity( + self.note_mcu_movequeue_activity( next_move_time + self.kin_flush_delay, set_step_gen_time=True ) self._advance_move_time(next_move_time) def _flush_lookahead(self): # Transit from "NeedPrime"/"Priming"/"Drip"/main state to "NeedPrime" - self.move_queue.flush() + self.lookahead.flush() self.special_queuing_state = "NeedPrime" self.need_check_pause = -1.0 - self.move_queue.set_flush_time(BUFFER_TIME_HIGH) + self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.check_stall_time = 0.0 def flush_step_generation(self): self._flush_lookahead() self._advance_flush_time(self.step_gen_time) + self.min_restart_time = max(self.min_restart_time, self.print_time) def get_last_move_time(self): if self.special_queuing_state: self._flush_lookahead() self._calc_print_time() else: - self.move_queue.flush() + self.lookahead.flush() return self.print_time def _check_pause(self): @@ -512,14 +525,15 @@ def _flush_handler(self, eventtime): self.check_stall_time = self.print_time # In "NeedPrime"/"Priming" state - flush queues if needed while 1: - if self.last_flush_time >= self.need_flush_time: + end_flush = self.need_flush_time + BGFLUSH_EXTRA_TIME + if self.last_flush_time >= end_flush: self.do_kick_flush_timer = True return self.reactor.NEVER buffer_time = self.last_flush_time - est_print_time if buffer_time > BGFLUSH_LOW_TIME: return eventtime + buffer_time - BGFLUSH_LOW_TIME ftime = est_print_time + BGFLUSH_LOW_TIME + BGFLUSH_BATCH_TIME - self._advance_flush_time(min(self.need_flush_time, ftime)) + self._advance_flush_time(min(end_flush, ftime)) except: logging.exception("Exception in flush_handler") self.printer.invoke_shutdown("Exception in flush_handler") @@ -548,7 +562,7 @@ def move(self, newpos, speed): if move.axes_d[3]: self.extruder.check_move(move) self.commanded_pos[:] = move.end_pos - self.move_queue.add_move(move) + self.lookahead.add_move(move) if self.print_time > self.need_check_pause: self._check_pause() @@ -597,7 +611,7 @@ def _update_drip_move_time(self, next_print_time): self.drip_completion.wait(curtime + wait_time) continue npt = min(self.print_time + DRIP_SEGMENT_TIME, next_print_time) - self.note_kinematic_activity( + self.note_mcu_movequeue_activity( npt + self.kin_flush_delay, set_step_gen_time=True ) self._advance_move_time(npt) @@ -605,12 +619,12 @@ def _update_drip_move_time(self, next_print_time): def drip_move(self, newpos, speed, drip_completion): self.dwell(self.kin_flush_delay) # Transition from "NeedPrime"/"Priming"/main state to "Drip" state - self.move_queue.flush() + self.lookahead.flush() self.special_queuing_state = "Drip" self.need_check_pause = self.reactor.NEVER self.reactor.update_timer(self.flush_timer, self.reactor.NEVER) self.do_kick_flush_timer = False - self.move_queue.set_flush_time(BUFFER_TIME_HIGH) + self.lookahead.set_flush_time(BUFFER_TIME_HIGH) self.check_stall_time = 0.0 self.drip_completion = drip_completion # Submit move @@ -622,10 +636,10 @@ def drip_move(self, newpos, speed, drip_completion): raise # Transmit move in "drip" mode try: - self.move_queue.flush() + self.lookahead.flush() except DripModeEndSignal as e: - self.move_queue.reset() - self.trapq_finalize_moves(self.trapq, self.reactor.NEVER) + self.lookahead.reset() + self.trapq_finalize_moves(self.trapq, self.reactor.NEVER, 0) # Exit "Drip" state self.reactor.update_timer(self.flush_timer, self.reactor.NOW) self.flush_step_generation() @@ -635,7 +649,9 @@ def stats(self, eventtime): max_queue_time = max(self.print_time, self.last_flush_time) for m in self.all_mcus: m.check_active(max_queue_time, eventtime) - buffer_time = self.print_time - self.mcu.estimated_print_time(eventtime) + est_print_time = self.mcu.estimated_print_time(eventtime) + self.clear_history_time = est_print_time - MOVE_HISTORY_EXPIRE + buffer_time = self.print_time - est_print_time is_active = buffer_time > -60.0 or not self.special_queuing_state if self.special_queuing_state == "Drip": buffer_time = 0.0 @@ -647,7 +663,7 @@ def stats(self, eventtime): def check_busy(self, eventtime): est_print_time = self.mcu.estimated_print_time(eventtime) - lookahead_empty = not self.move_queue.queue + lookahead_empty = not self.lookahead.queue return self.print_time, est_print_time, lookahead_empty def get_status(self, eventtime): @@ -671,7 +687,7 @@ def get_status(self, eventtime): def _handle_shutdown(self): self.can_pause = False - self.move_queue.reset() + self.lookahead.reset() def get_kinematics(self): return self.kin @@ -693,16 +709,16 @@ def note_step_generation_scan_time(self, delay, old_delay=0.0): self.kin_flush_delay = new_delay def register_lookahead_callback(self, callback): - last_move = self.move_queue.get_last() + last_move = self.lookahead.get_last() if last_move is None: callback(self.get_last_move_time()) return last_move.timing_callbacks.append(callback) - def note_kinematic_activity(self, kin_time, set_step_gen_time=False): - self.need_flush_time = max(self.need_flush_time, kin_time) + def note_mcu_movequeue_activity(self, mq_time, set_step_gen_time=False): + self.need_flush_time = max(self.need_flush_time, mq_time) if set_step_gen_time: - self.step_gen_time = max(self.step_gen_time, kin_time) + self.step_gen_time = max(self.step_gen_time, mq_time) if self.do_kick_flush_timer: self.do_kick_flush_timer = False self.reactor.update_timer(self.flush_timer, self.reactor.NOW) diff --git a/src/Kconfig b/src/Kconfig index c4a9d8828..a98c52483 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -114,6 +114,10 @@ config WANT_SOFTWARE_SPI bool depends on HAVE_GPIO && HAVE_GPIO_SPI default y +config NEED_SENSOR_BULK + bool + depends on WANT_SENSORS || WANT_LIS2DW + default y menu "Optional features (to reduce code size)" depends on HAVE_LIMITED_CODE_SIZE config WANT_GPIO_BITBANGING diff --git a/src/Makefile b/src/Makefile index 7bd3893b2..7d7f74e82 100644 --- a/src/Makefile +++ b/src/Makefile @@ -19,3 +19,5 @@ sensors-src-$(CONFIG_HAVE_GPIO_SPI) := thermocouple.c sensor_adxl345.c \ src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y) +src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c +src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c diff --git a/src/adccmds.c b/src/adccmds.c index a27dc8c06..5a443aea2 100644 --- a/src/adccmds.c +++ b/src/adccmds.c @@ -43,13 +43,15 @@ analog_in_event(struct timer *timer) a->timer.waketime += a->sample_time; return SF_RESCHEDULE; } - if (likely(a->value >= a->min_value && a->value <= a->max_value)) { - a->invalid_count = 0; - } else { - a->invalid_count++; - if (a->invalid_count >= a->range_check_count) { - try_shutdown("ADC out of range"); + if (a->range_check_count > 0) { + if (likely(a->value >= a->min_value && a->value <= a->max_value)) { a->invalid_count = 0; + } else { + a->invalid_count++; + if (a->invalid_count >= a->range_check_count) { + try_shutdown("ADC out of range"); + a->invalid_count = 0; + } } } sched_wake_task(&analog_wake); diff --git a/src/generic/armcm_boot.c b/src/generic/armcm_boot.c index f83ca60de..9d2ce0bbf 100644 --- a/src/generic/armcm_boot.c +++ b/src/generic/armcm_boot.c @@ -22,7 +22,31 @@ extern uint32_t _stack_end; * Basic interrupt handlers ****************************************************************/ -static void __noreturn +// Inlined version of memset (to avoid function calls during intial boot code) +static void __always_inline +boot_memset(void *s, int c, size_t n) +{ + volatile uint32_t *p = s; + while (n) { + *p++ = c; + n -= sizeof(*p); + } +} + +// Inlined version of memcpy (to avoid function calls during intial boot code) +static void __always_inline +boot_memcpy(void *dest, const void *src, size_t n) +{ + const uint32_t *s = src; + volatile uint32_t *d = dest; + while (n) { + *d++ = *s++; + n -= sizeof(*d); + } +} + +// Main initialization code (called from ResetHandler below) +static void __noreturn __section(".text.armcm_boot.stage_two") reset_handler_stage_two(void) { int i; @@ -60,10 +84,10 @@ reset_handler_stage_two(void) // Copy global variables from flash to ram uint32_t count = (&_data_end - &_data_start) * 4; - __builtin_memcpy(&_data_start, &_data_flash, count); + boot_memcpy(&_data_start, &_data_flash, count); // Clear the bss segment - __builtin_memset(&_bss_start, 0, (&_bss_end - &_bss_start) * 4); + boot_memset(&_bss_start, 0, (&_bss_end - &_bss_start) * 4); barrier(); @@ -80,7 +104,7 @@ reset_handler_stage_two(void) // Initial code entry point - invoked by the processor after a reset // Reset interrupts and stack to take control from bootloaders -void +void __section(".text.armcm_boot.stage_one") ResetHandler(void) { __disable_irq(); diff --git a/src/rp2040/Makefile b/src/rp2040/Makefile index 71ed90a0c..641990140 100644 --- a/src/rp2040/Makefile +++ b/src/rp2040/Makefile @@ -55,7 +55,7 @@ $(OUT)klipper.bin: $(OUT)klipper.elf $(Q)$(OBJCOPY) -O binary $< $@ rptarget-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)klipper.bin -rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/generic/armcm_link.ld +rplink-$(CONFIG_RP2040_HAVE_BOOTLOADER) := $(OUT)src/rp2040/rp2040_link.ld # Set klipper.elf linker rules target-y += $(rptarget-y) diff --git a/src/rp2040/main.c b/src/rp2040/main.c index 0b144d0bb..e7b64e5f0 100644 --- a/src/rp2040/main.c +++ b/src/rp2040/main.c @@ -16,6 +16,26 @@ #include "sched.h" // sched_main +/**************************************************************** + * Ram IRQ vector table + ****************************************************************/ + +// Copy vector table to ram and activate it +static void +enable_ram_vectortable(void) +{ + // Symbols created by rp2040_link.lds.S linker script + extern uint32_t _ram_vectortable_start, _ram_vectortable_end; + extern uint32_t _text_vectortable_start; + + uint32_t count = (&_ram_vectortable_end - &_ram_vectortable_start) * 4; + __builtin_memcpy(&_ram_vectortable_start, &_text_vectortable_start, count); + barrier(); + + SCB->VTOR = (uint32_t)&_ram_vectortable_start; +} + + /**************************************************************** * Bootloader ****************************************************************/ @@ -145,6 +165,7 @@ clock_setup(void) void armcm_main(void) { + enable_ram_vectortable(); clock_setup(); sched_main(); } diff --git a/src/rp2040/rp2040_link.lds.S b/src/rp2040/rp2040_link.lds.S index 43d6115e4..9b0264a2b 100644 --- a/src/rp2040/rp2040_link.lds.S +++ b/src/rp2040/rp2040_link.lds.S @@ -1,6 +1,6 @@ // rp2040 linker script (based on armcm_link.lds.S and customized for stage2) // -// Copyright (C) 2019-2021 Kevin O'Connor +// Copyright (C) 2019-2024 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -9,9 +9,15 @@ OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) +#if CONFIG_RP2040_HAVE_STAGE2 + #define ROM_ORIGIN 0x10000000 +#else + #define ROM_ORIGIN CONFIG_FLASH_APPLICATION_ADDRESS +#endif + MEMORY { - rom (rx) : ORIGIN = 0x10000000 , LENGTH = CONFIG_FLASH_SIZE + rom (rx) : ORIGIN = ROM_ORIGIN , LENGTH = CONFIG_FLASH_SIZE ram (rwx) : ORIGIN = CONFIG_RAM_START , LENGTH = CONFIG_RAM_SIZE } @@ -19,22 +25,31 @@ SECTIONS { .text : { . = ALIGN(4); +#if CONFIG_RP2040_HAVE_STAGE2 KEEP(*(.boot2)) +#endif _text_vectortable_start = .; KEEP(*(.vector_table)) _text_vectortable_end = .; - *(.text .text.*) - *(.rodata .rodata*) + *(.text.armcm_boot*) } > rom . = ALIGN(4); _data_flash = .; + .ram_vectortable (NOLOAD) : { + _ram_vectortable_start = .; + . = . + ( _text_vectortable_end - _text_vectortable_start ) ; + _ram_vectortable_end = .; + } > ram + .data : AT (_data_flash) { . = ALIGN(4); _data_start = .; + *(.text .text.*) *(.ramfunc .ramfunc.*); + *(.rodata .rodata*) *(.data .data.*); . = ALIGN(4); _data_end = .; diff --git a/src/rp2040/spi.c b/src/rp2040/spi.c index e6aafa005..758d57308 100644 --- a/src/rp2040/spi.c +++ b/src/rp2040/spi.c @@ -89,8 +89,12 @@ void spi_prepare(struct spi_config config) { spi_hw_t *spi = config.spi; + if (spi->cr0 == config.cr0 && spi->cpsr == config.cpsr) + return; + spi->cr1 = 0; spi->cr0 = config.cr0; spi->cpsr = config.cpsr; + spi->cr1 = SPI_SSPCR1_SSE_BITS; } void diff --git a/src/sensor_adxl345.c b/src/sensor_adxl345.c index 3d80059d0..32ce4c653 100644 --- a/src/sensor_adxl345.c +++ b/src/sensor_adxl345.c @@ -1,6 +1,6 @@ // Support for gathering acceleration data from ADXL345 chip // -// Copyright (C) 2020 Kevin O'Connor +// Copyright (C) 2020-2023 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -10,19 +10,19 @@ #include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report #include "spicmds.h" // spidev_transfer struct adxl345 { struct timer timer; uint32_t rest_ticks; struct spidev_s *spi; - uint16_t sequence, limit_count; - uint8_t flags, data_count; - uint8_t data[50]; + uint8_t flags; + struct sensor_bulk sb; }; enum { - AX_HAVE_START = 1<<0, AX_RUNNING = 1<<1, AX_PENDING = 1<<2, + AX_PENDING = 1<<0, }; static struct task_wake adxl345_wake; @@ -47,27 +47,6 @@ command_config_adxl345(uint32_t *args) } DECL_COMMAND(command_config_adxl345, "config_adxl345 oid=%c spi_oid=%c"); -// Report local measurement buffer -static void -adxl_report(struct adxl345 *ax, uint8_t oid) -{ - sendf("adxl345_data oid=%c sequence=%hu data=%*s" - , oid, ax->sequence, ax->data_count, ax->data); - ax->data_count = 0; - ax->sequence++; -} - -// Report buffer and fifo status -static void -adxl_status(struct adxl345 *ax, uint_fast8_t oid - , uint32_t time1, uint32_t time2, uint_fast8_t fifo) -{ - sendf("adxl345_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%c limit_count=%hu" - , oid, time1, time2-time1, ax->sequence - , ax->data_count, fifo, ax->limit_count); -} - // Helper code to reschedule the adxl345_event() timer static void adxl_reschedule_timer(struct adxl345 *ax) @@ -79,7 +58,6 @@ adxl_reschedule_timer(struct adxl345 *ax) } // Chip registers -#define AR_POWER_CTL 0x2D #define AR_DATAX0 0x32 #define AR_FIFO_STATUS 0x39 #define AM_READ 0x80 @@ -87,6 +65,8 @@ adxl_reschedule_timer(struct adxl345 *ax) #define SET_FIFO_CTL 0x90 +#define BYTES_PER_SAMPLE 5 + // Query accelerometer data static void adxl_query(struct adxl345 *ax, uint8_t oid) @@ -96,7 +76,7 @@ adxl_query(struct adxl345 *ax, uint8_t oid) spidev_transfer(ax->spi, 1, sizeof(msg), msg); // Extract x, y, z measurements uint_fast8_t fifo_status = msg[8] & ~0x80; // Ignore trigger bit - uint8_t *d = &ax->data[ax->data_count]; + uint8_t *d = &ax->sb.data[ax->sb.data_count]; if (((msg[2] & 0xf0) && (msg[2] & 0xf0) != 0xf0) || ((msg[4] & 0xf0) && (msg[4] & 0xf0) != 0xf0) || ((msg[6] & 0xf0) && (msg[6] & 0xf0) != 0xf0) @@ -112,94 +92,56 @@ adxl_query(struct adxl345 *ax, uint8_t oid) d[3] = (msg[2] & 0x1f) | (msg[6] << 5); // x high bits and z high bits d[4] = (msg[4] & 0x1f) | ((msg[6] << 2) & 0x60); // y high and z high } - ax->data_count += 5; - if (ax->data_count + 5 > ARRAY_SIZE(ax->data)) - adxl_report(ax, oid); + ax->sb.data_count += BYTES_PER_SAMPLE; + if (ax->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ax->sb.data)) + sensor_bulk_report(&ax->sb, oid); // Check fifo status if (fifo_status >= 31) - ax->limit_count++; - if (fifo_status > 1 && fifo_status <= 32) { + ax->sb.possible_overflows++; + if (fifo_status > 1) { // More data in fifo - wake this task again sched_wake_task(&adxl345_wake); - } else if (ax->flags & AX_RUNNING) { + } else { // Sleep until next check time - sched_del_timer(&ax->timer); ax->flags &= ~AX_PENDING; adxl_reschedule_timer(ax); } } -// Startup measurements -static void -adxl_start(struct adxl345 *ax, uint8_t oid) -{ - sched_del_timer(&ax->timer); - ax->flags = AX_RUNNING; - uint8_t msg[2] = { AR_POWER_CTL, 0x08 }; - spidev_transfer(ax->spi, 0, sizeof(msg), msg); - adxl_reschedule_timer(ax); -} - -// End measurements -static void -adxl_stop(struct adxl345 *ax, uint8_t oid) -{ - // Disable measurements - sched_del_timer(&ax->timer); - ax->flags = 0; - uint8_t msg[2] = { AR_POWER_CTL, 0x00 }; - uint32_t end1_time = timer_read_time(); - spidev_transfer(ax->spi, 0, sizeof(msg), msg); - uint32_t end2_time = timer_read_time(); - // Drain any measurements still in fifo - uint_fast8_t i; - for (i=0; i<33; i++) { - msg[0] = AR_FIFO_STATUS | AM_READ; - msg[1] = 0x00; - spidev_transfer(ax->spi, 1, sizeof(msg), msg); - uint_fast8_t fifo_status = msg[1] & ~0x80; - if (!fifo_status) - break; - if (fifo_status <= 32) - adxl_query(ax, oid); - } - // Report final data - if (ax->data_count) - adxl_report(ax, oid); - adxl_status(ax, oid, end1_time, end2_time, msg[1]); -} - void command_query_adxl345(uint32_t *args) { struct adxl345 *ax = oid_lookup(args[0], command_config_adxl345); - if (!args[2]) { + sched_del_timer(&ax->timer); + ax->flags = 0; + if (!args[1]) // End measurements - adxl_stop(ax, args[0]); return; - } + // Start new measurements query - sched_del_timer(&ax->timer); - ax->timer.waketime = args[1]; - ax->rest_ticks = args[2]; - ax->flags = AX_HAVE_START; - ax->sequence = ax->limit_count = 0; - ax->data_count = 0; - sched_add_timer(&ax->timer); + ax->rest_ticks = args[1]; + sensor_bulk_reset(&ax->sb); + adxl_reschedule_timer(ax); } -DECL_COMMAND(command_query_adxl345, - "query_adxl345 oid=%c clock=%u rest_ticks=%u"); +DECL_COMMAND(command_query_adxl345, "query_adxl345 oid=%c rest_ticks=%u"); void command_query_adxl345_status(uint32_t *args) { struct adxl345 *ax = oid_lookup(args[0], command_config_adxl345); uint8_t msg[2] = { AR_FIFO_STATUS | AM_READ, 0x00 }; + uint32_t time1 = timer_read_time(); spidev_transfer(ax->spi, 1, sizeof(msg), msg); uint32_t time2 = timer_read_time(); - adxl_status(ax, args[0], time1, time2, msg[1]); + + uint_fast8_t fifo_status = msg[1] & ~0x80; // Ignore trigger bit + if (fifo_status > 32) + // Query error - don't send response - host will retry + return; + sensor_bulk_status(&ax->sb, args[0], time1, time2-time1 + , fifo_status * BYTES_PER_SAMPLE); } DECL_COMMAND(command_query_adxl345_status, "query_adxl345_status oid=%c"); @@ -212,11 +154,7 @@ adxl345_task(void) struct adxl345 *ax; foreach_oid(oid, ax, command_config_adxl345) { uint_fast8_t flags = ax->flags; - if (!(flags & AX_PENDING)) - continue; - if (flags & AX_HAVE_START) - adxl_start(ax, oid); - else + if (flags & AX_PENDING) adxl_query(ax, oid); } } diff --git a/src/sensor_angle.c b/src/sensor_angle.c index 4d35aadf1..54caecc21 100644 --- a/src/sensor_angle.c +++ b/src/sensor_angle.c @@ -10,6 +10,7 @@ #include "board/irq.h" // irq_disable #include "command.h" // DECL_COMMAND #include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report #include "spicmds.h" // spidev_transfer enum { SA_CHIP_A1333, SA_CHIP_AS5047D, SA_CHIP_TLE5012B, SA_CHIP_MAX }; @@ -29,15 +30,16 @@ struct spi_angle { struct timer timer; uint32_t rest_ticks; struct spidev_s *spi; - uint16_t sequence; - uint8_t flags, chip_type, data_count, time_shift, overflow; - uint8_t data[48]; + uint8_t flags, chip_type, time_shift, overflow; + struct sensor_bulk sb; }; enum { SA_PENDING = 1<<2, }; +#define BYTES_PER_SAMPLE 3 + static struct task_wake angle_wake; // Event handler that wakes spi_angle_task() periodically @@ -72,32 +74,22 @@ command_config_spi_angle(uint32_t *args) DECL_COMMAND(command_config_spi_angle, "config_spi_angle oid=%c spi_oid=%c spi_angle_type=%c"); -// Report local measurement buffer -static void -angle_report(struct spi_angle *sa, uint8_t oid) -{ - sendf("spi_angle_data oid=%c sequence=%hu data=%*s" - , oid, sa->sequence, sa->data_count, sa->data); - sa->data_count = 0; - sa->sequence++; -} - // Send spi_angle_data message if buffer is full static void angle_check_report(struct spi_angle *sa, uint8_t oid) { - if (sa->data_count + 3 > ARRAY_SIZE(sa->data)) - angle_report(sa, oid); + if (sa->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(sa->sb.data)) + sensor_bulk_report(&sa->sb, oid); } // Add an entry to the measurement buffer static void angle_add(struct spi_angle *sa, uint_fast8_t tcode, uint_fast16_t data) { - sa->data[sa->data_count] = tcode; - sa->data[sa->data_count + 1] = data; - sa->data[sa->data_count + 2] = data >> 8; - sa->data_count += 3; + sa->sb.data[sa->sb.data_count] = tcode; + sa->sb.data[sa->sb.data_count + 1] = data; + sa->sb.data[sa->sb.data_count + 2] = data >> 8; + sa->sb.data_count += BYTES_PER_SAMPLE; } // Add an error indicator to the measurement buffer @@ -230,18 +222,14 @@ command_query_spi_angle(uint32_t *args) sched_del_timer(&sa->timer); sa->flags = 0; - if (!args[2]) { + if (!args[2]) // End measurements - if (sa->data_count) - angle_report(sa, oid); - sendf("spi_angle_end oid=%c sequence=%hu", oid, sa->sequence); return; - } + // Start new measurements query sa->timer.waketime = args[1]; sa->rest_ticks = args[2]; - sa->sequence = 0; - sa->data_count = 0; + sensor_bulk_reset(&sa->sb); sa->time_shift = args[3]; sched_add_timer(&sa->timer); } diff --git a/src/sensor_bulk.c b/src/sensor_bulk.c new file mode 100644 index 000000000..9b5c782c5 --- /dev/null +++ b/src/sensor_bulk.c @@ -0,0 +1,38 @@ +// Helper code for collecting and sending bulk sensor measurements +// +// Copyright (C) 2020-2023 Kevin O'Connor +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "command.h" // sendf +#include "sensor_bulk.h" // sensor_bulk_report + +// Reset counters +void +sensor_bulk_reset(struct sensor_bulk *sb) +{ + sb->sequence = 0; + sb->possible_overflows = 0; + sb->data_count = 0; +} + +// Report local measurement buffer +void +sensor_bulk_report(struct sensor_bulk *sb, uint8_t oid) +{ + sendf("sensor_bulk_data oid=%c sequence=%hu data=%*s" + , oid, sb->sequence, sb->data_count, sb->data); + sb->data_count = 0; + sb->sequence++; +} + +// Report buffer and fifo status +void +sensor_bulk_status(struct sensor_bulk *sb, uint8_t oid + , uint32_t time1, uint32_t query_ticks, uint32_t fifo) +{ + sendf("sensor_bulk_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" + " buffered=%u possible_overflows=%hu" + , oid, time1, query_ticks, sb->sequence + , sb->data_count + fifo, sb->possible_overflows); +} diff --git a/src/sensor_bulk.h b/src/sensor_bulk.h new file mode 100644 index 000000000..9c130bea3 --- /dev/null +++ b/src/sensor_bulk.h @@ -0,0 +1,15 @@ +#ifndef __SENSOR_BULK_H +#define __SENSOR_BULK_H + +struct sensor_bulk { + uint16_t sequence, possible_overflows; + uint8_t data_count; + uint8_t data[52]; +}; + +void sensor_bulk_reset(struct sensor_bulk *sb); +void sensor_bulk_report(struct sensor_bulk *sb, uint8_t oid); +void sensor_bulk_status(struct sensor_bulk *sb, uint8_t oid + , uint32_t time1, uint32_t query_ticks, uint32_t fifo); + +#endif // sensor_bulk.h diff --git a/src/sensor_lis2dw.c b/src/sensor_lis2dw.c index 52612623f..83922003c 100644 --- a/src/sensor_lis2dw.c +++ b/src/sensor_lis2dw.c @@ -1,7 +1,7 @@ // Support for gathering acceleration data from LIS2DW chip // // Copyright (C) 2023 Zhou.XianMing -// Copyright (C) 2020 Kevin O'Connor +// Copyright (C) 2020-2023 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -11,24 +11,25 @@ #include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report #include "spicmds.h" // spidev_transfer #define LIS_AR_DATAX0 0x28 #define LIS_AM_READ 0x80 -#define LIS_FIFO_CTRL 0x2E #define LIS_FIFO_SAMPLES 0x2F +#define BYTES_PER_SAMPLE 6 + struct lis2dw { struct timer timer; uint32_t rest_ticks; struct spidev_s *spi; - uint16_t sequence, limit_count; - uint8_t flags, data_count, fifo_disable; - uint8_t data[48]; + uint8_t flags; + struct sensor_bulk sb; }; enum { - LIS_HAVE_START = 1<<0, LIS_RUNNING = 1<<1, LIS_PENDING = 1<<2, + LIS_PENDING = 1<<0, }; static struct task_wake lis2dw_wake; @@ -53,27 +54,6 @@ command_config_lis2dw(uint32_t *args) } DECL_COMMAND(command_config_lis2dw, "config_lis2dw oid=%c spi_oid=%c"); -// Report local measurement buffer -static void -lis2dw_report(struct lis2dw *ax, uint8_t oid) -{ - sendf("lis2dw_data oid=%c sequence=%hu data=%*s" - , oid, ax->sequence, ax->data_count, ax->data); - ax->data_count = 0; - ax->sequence++; -} - -// Report buffer and fifo status -static void -lis2dw_status(struct lis2dw *ax, uint_fast8_t oid - , uint32_t time1, uint32_t time2, uint_fast8_t fifo) -{ - sendf("lis2dw_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%c limit_count=%hu" - , oid, time1, time2-time1, ax->sequence - , ax->data_count, fifo, ax->limit_count); -} - // Helper code to reschedule the lis2dw_event() timer static void lis2dw_reschedule_timer(struct lis2dw *ax) @@ -93,7 +73,7 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid) uint8_t fifo_empty,fifo_ovrn = 0; msg[0] = LIS_AR_DATAX0 | LIS_AM_READ ; - uint8_t *d = &ax->data[ax->data_count]; + uint8_t *d = &ax->sb.data[ax->sb.data_count]; spidev_transfer(ax->spi, 1, sizeof(msg), msg); @@ -108,86 +88,42 @@ lis2dw_query(struct lis2dw *ax, uint8_t oid) d[4] = msg[5]; // z low bits d[5] = msg[6]; // z high bits - ax->data_count += 6; - if (ax->data_count + 6 > ARRAY_SIZE(ax->data)) - lis2dw_report(ax, oid); + ax->sb.data_count += BYTES_PER_SAMPLE; + if (ax->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(ax->sb.data)) + sensor_bulk_report(&ax->sb, oid); // Check fifo status if (fifo_ovrn) - ax->limit_count++; + ax->sb.possible_overflows++; // check if we need to run the task again (more packets in fifo?) - if (!fifo_empty&&!(ax->fifo_disable)) { + if (!fifo_empty) { // More data in fifo - wake this task again sched_wake_task(&lis2dw_wake); - } else if (ax->flags & LIS_RUNNING) { + } else { // Sleep until next check time - sched_del_timer(&ax->timer); ax->flags &= ~LIS_PENDING; lis2dw_reschedule_timer(ax); } } -// Startup measurements -static void -lis2dw_start(struct lis2dw *ax, uint8_t oid) -{ - sched_del_timer(&ax->timer); - ax->flags = LIS_RUNNING; - ax->fifo_disable = 0; - uint8_t ctrl[2] = {LIS_FIFO_CTRL , 0xC0}; - spidev_transfer(ax->spi, 0, sizeof(ctrl), ctrl); - lis2dw_reschedule_timer(ax); -} - -// End measurements -static void -lis2dw_stop(struct lis2dw *ax, uint8_t oid) -{ - // Disable measurements - sched_del_timer(&ax->timer); - ax->flags = 0; - // Drain any measurements still in fifo - ax->fifo_disable = 1; - lis2dw_query(ax, oid); - - uint8_t ctrl[2] = {LIS_FIFO_CTRL , 0}; - uint32_t end1_time = timer_read_time(); - spidev_transfer(ax->spi, 0, sizeof(ctrl), ctrl); - uint32_t end2_time = timer_read_time(); - - uint8_t msg[2] = { LIS_FIFO_SAMPLES | LIS_AM_READ , 0}; - spidev_transfer(ax->spi, 1, sizeof(msg), msg); - uint8_t fifo_status = msg[1]&0x1f; - - //Report final data - if (ax->data_count) - lis2dw_report(ax, oid); - lis2dw_status(ax, oid, end1_time, end2_time, fifo_status); -} - void command_query_lis2dw(uint32_t *args) { struct lis2dw *ax = oid_lookup(args[0], command_config_lis2dw); - if (!args[2]) { + sched_del_timer(&ax->timer); + ax->flags = 0; + if (!args[1]) // End measurements - lis2dw_stop(ax, args[0]); return; - } + // Start new measurements query - sched_del_timer(&ax->timer); - ax->timer.waketime = args[1]; - ax->rest_ticks = args[2]; - ax->flags = LIS_HAVE_START; - ax->sequence = ax->limit_count = 0; - ax->data_count = 0; - ax->fifo_disable = 0; - sched_add_timer(&ax->timer); + ax->rest_ticks = args[1]; + sensor_bulk_reset(&ax->sb); + lis2dw_reschedule_timer(ax); } -DECL_COMMAND(command_query_lis2dw, - "query_lis2dw oid=%c clock=%u rest_ticks=%u"); +DECL_COMMAND(command_query_lis2dw, "query_lis2dw oid=%c rest_ticks=%u"); void command_query_lis2dw_status(uint32_t *args) @@ -197,7 +133,8 @@ command_query_lis2dw_status(uint32_t *args) uint32_t time1 = timer_read_time(); spidev_transfer(ax->spi, 1, sizeof(msg), msg); uint32_t time2 = timer_read_time(); - lis2dw_status(ax, args[0], time1, time2, msg[1]&0x1f); + sensor_bulk_status(&ax->sb, args[0], time1, time2-time1 + , (msg[1] & 0x1f) * BYTES_PER_SAMPLE); } DECL_COMMAND(command_query_lis2dw_status, "query_lis2dw_status oid=%c"); @@ -210,11 +147,7 @@ lis2dw_task(void) struct lis2dw *ax; foreach_oid(oid, ax, command_config_lis2dw) { uint_fast8_t flags = ax->flags; - if (!(flags & LIS_PENDING)) - continue; - if (flags & LIS_HAVE_START) - lis2dw_start(ax, oid); - else + if (flags & LIS_PENDING) lis2dw_query(ax, oid); } } diff --git a/src/sensor_mpu9250.c b/src/sensor_mpu9250.c index 51df5a711..23c029211 100644 --- a/src/sensor_mpu9250.c +++ b/src/sensor_mpu9250.c @@ -2,7 +2,7 @@ // // Copyright (C) 2023 Matthew Swabey // Copyright (C) 2022 Harry Beyel -// Copyright (C) 2020-2021 Kevin O'Connor +// Copyright (C) 2020-2023 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -12,65 +12,35 @@ #include "basecmd.h" // oid_alloc #include "command.h" // DECL_COMMAND #include "sched.h" // DECL_TASK +#include "sensor_bulk.h" // sensor_bulk_report #include "board/gpio.h" // i2c_read #include "i2ccmds.h" // i2cdev_oid_lookup // Chip registers -#define AR_FIFO_SIZE 512 - -#define AR_PWR_MGMT_1 0x6B -#define AR_PWR_MGMT_2 0x6C -#define AR_FIFO_EN 0x23 -#define AR_ACCEL_OUT_XH 0x3B -#define AR_USER_CTRL 0x6A #define AR_FIFO_COUNT_H 0x72 #define AR_FIFO 0x74 #define AR_INT_STATUS 0x3A -#define SET_ENABLE_FIFO 0x08 -#define SET_DISABLE_FIFO 0x00 -#define SET_USER_FIFO_RESET 0x04 -#define SET_USER_FIFO_EN 0x40 - -#define SET_PWR_SLEEP 0x40 -#define SET_PWR_WAKE 0x00 -#define SET_PWR_2_ACCEL 0x07 // only enable accelerometers -#define SET_PWR_2_NONE 0x3F // disable all sensors - #define FIFO_OVERFLOW_INT 0x10 #define BYTES_PER_FIFO_ENTRY 6 +#define BYTES_PER_BLOCK 48 struct mpu9250 { struct timer timer; uint32_t rest_ticks; struct i2cdev_s *i2c; - uint16_t sequence, limit_count, fifo_max, fifo_pkts_bytes; - uint8_t flags, data_count; - // msg size must be <= 255 due to Klipper api - // = SAMPLES_PER_BLOCK (from mpu9250.py) * BYTES_PER_FIFO_ENTRY + 1 - uint8_t data[48]; + uint16_t fifo_max, fifo_pkts_bytes; + uint8_t flags; + struct sensor_bulk sb; }; enum { - AX_HAVE_START = 1<<0, AX_RUNNING = 1<<1, AX_PENDING = 1<<2, + AX_PENDING = 1<<0, }; static struct task_wake mpu9250_wake; -// Reads the fifo byte count from the device. -static uint16_t -get_fifo_status (struct mpu9250 *mp) -{ - uint8_t reg[] = {AR_FIFO_COUNT_H}; - uint8_t msg[2]; - i2c_read(mp->i2c->i2c_config, sizeof(reg), reg, sizeof(msg), msg); - msg[0] = 0x1F & msg[0]; // discard 3 MSB per datasheet - uint16_t bytes_to_read = ((uint16_t)msg[0]) << 8 | msg[1]; - if (bytes_to_read > mp->fifo_max) mp->fifo_max = bytes_to_read; - return bytes_to_read; -} - // Event handler that wakes mpu9250_task() periodically static uint_fast8_t mpu9250_event(struct timer *timer) @@ -91,27 +61,6 @@ command_config_mpu9250(uint32_t *args) } DECL_COMMAND(command_config_mpu9250, "config_mpu9250 oid=%c i2c_oid=%c"); -// Report local measurement buffer -static void -mp9250_report(struct mpu9250 *mp, uint8_t oid) -{ - sendf("mpu9250_data oid=%c sequence=%hu data=%*s" - , oid, mp->sequence, mp->data_count, mp->data); - mp->data_count = 0; - mp->sequence++; -} - -// Report buffer and fifo status -static void -mp9250_status(struct mpu9250 *mp, uint_fast8_t oid - , uint32_t time1, uint32_t time2, uint16_t fifo) -{ - sendf("mpu9250_status oid=%c clock=%u query_ticks=%u next_sequence=%hu" - " buffered=%c fifo=%u limit_count=%hu" - , oid, time1, time2-time1, mp->sequence - , mp->data_count, fifo, mp->limit_count); -} - // Helper code to reschedule the mpu9250_event() timer static void mp9250_reschedule_timer(struct mpu9250 *mp) @@ -122,141 +71,94 @@ mp9250_reschedule_timer(struct mpu9250 *mp) irq_enable(); } +// Reads the fifo byte count from the device. +static uint16_t +get_fifo_status(struct mpu9250 *mp) +{ + uint8_t reg[] = {AR_FIFO_COUNT_H}; + uint8_t msg[2]; + i2c_read(mp->i2c->i2c_config, sizeof(reg), reg, sizeof(msg), msg); + uint16_t fifo_bytes = ((msg[0] & 0x1f) << 8) | msg[1]; + if (fifo_bytes > mp->fifo_max) + mp->fifo_max = fifo_bytes; + return fifo_bytes; +} + // Query accelerometer data static void mp9250_query(struct mpu9250 *mp, uint8_t oid) { - // Find remaining space in report buffer - uint8_t data_space = sizeof(mp->data) - mp->data_count; - // If not enough bytes to fill report read MPU FIFO's fill - if (mp->fifo_pkts_bytes < data_space) { - mp->fifo_pkts_bytes = get_fifo_status(mp) / BYTES_PER_FIFO_ENTRY - * BYTES_PER_FIFO_ENTRY; - } + if (mp->fifo_pkts_bytes < BYTES_PER_BLOCK) + mp->fifo_pkts_bytes = get_fifo_status(mp); // If we have enough bytes to fill the buffer do it and send report - if (mp->fifo_pkts_bytes >= data_space) { + if (mp->fifo_pkts_bytes >= BYTES_PER_BLOCK) { uint8_t reg = AR_FIFO; - i2c_read(mp->i2c->i2c_config, sizeof(reg), ®, - data_space, &mp->data[mp->data_count]); - mp->data_count += data_space; - mp->fifo_pkts_bytes -= data_space; - mp9250_report(mp, oid); + i2c_read(mp->i2c->i2c_config, sizeof(reg), ® + , BYTES_PER_BLOCK, &mp->sb.data[0]); + mp->sb.data_count = BYTES_PER_BLOCK; + mp->fifo_pkts_bytes -= BYTES_PER_BLOCK; + sensor_bulk_report(&mp->sb, oid); } // If we have enough bytes remaining to fill another report wake again // otherwise schedule timed wakeup - if (mp->fifo_pkts_bytes > data_space) { + if (mp->fifo_pkts_bytes >= BYTES_PER_BLOCK) { sched_wake_task(&mpu9250_wake); - } else if (mp->flags & AX_RUNNING) { - sched_del_timer(&mp->timer); + } else { mp->flags &= ~AX_PENDING; mp9250_reschedule_timer(mp); } } -// Startup measurements -static void -mp9250_start(struct mpu9250 *mp, uint8_t oid) -{ - sched_del_timer(&mp->timer); - mp->flags = AX_RUNNING; - uint8_t msg[2]; - - msg[0] = AR_FIFO_EN; - msg[1] = SET_DISABLE_FIFO; // disable FIFO - i2c_write(mp->i2c->i2c_config, sizeof(msg), msg); - - msg[0] = AR_USER_CTRL; - msg[1] = SET_USER_FIFO_RESET; // reset FIFO buffer - i2c_write(mp->i2c->i2c_config, sizeof(msg), msg); - - msg[0] = AR_USER_CTRL; - msg[1] = SET_USER_FIFO_EN; // enable FIFO buffer access - i2c_write(mp->i2c->i2c_config, sizeof(msg), msg); - - uint8_t int_reg[] = {AR_INT_STATUS}; // clear FIFO overflow flag - i2c_read(mp->i2c->i2c_config, sizeof(int_reg), int_reg, 1, msg); - - msg[0] = AR_FIFO_EN; - msg[1] = SET_ENABLE_FIFO; // enable accel output to FIFO - i2c_write(mp->i2c->i2c_config, sizeof(msg), msg); - - mp9250_reschedule_timer(mp); -} - -// End measurements -static void -mp9250_stop(struct mpu9250 *mp, uint8_t oid) -{ - // Disable measurements - sched_del_timer(&mp->timer); - mp->flags = 0; - - // disable accel FIFO - uint8_t msg[2] = { AR_FIFO_EN, SET_DISABLE_FIFO }; - uint32_t end1_time = timer_read_time(); - i2c_write(mp->i2c->i2c_config, sizeof(msg), msg); - uint32_t end2_time = timer_read_time(); - - // Detect if a FIFO overrun occured - uint8_t int_reg[] = {AR_INT_STATUS}; - uint8_t int_msg; - i2c_read(mp->i2c->i2c_config, sizeof(int_reg), int_reg, sizeof(int_msg), - &int_msg); - if (int_msg & FIFO_OVERFLOW_INT) - mp->limit_count++; - - // Report final data - if (mp->data_count > 0) - mp9250_report(mp, oid); - uint16_t bytes_to_read = get_fifo_status(mp); - mp9250_status(mp, oid, end1_time, end2_time, - bytes_to_read / BYTES_PER_FIFO_ENTRY); - - // Uncomment and rebuilt to check for FIFO overruns when tuning - //output("mpu9240 limit_count=%u fifo_max=%u", - // mp->limit_count, mp->fifo_max); -} - void command_query_mpu9250(uint32_t *args) { struct mpu9250 *mp = oid_lookup(args[0], command_config_mpu9250); - if (!args[2]) { + sched_del_timer(&mp->timer); + mp->flags = 0; + if (!args[1]) { // End measurements - mp9250_stop(mp, args[0]); + + // Uncomment and rebuilt to check for FIFO overruns when tuning + //output("mpu9240 fifo_max=%u", mp->fifo_max); return; } + // Start new measurements query - sched_del_timer(&mp->timer); - mp->timer.waketime = args[1]; - mp->rest_ticks = args[2]; - mp->flags = AX_HAVE_START; - mp->sequence = 0; - mp->limit_count = 0; - mp->data_count = 0; + mp->rest_ticks = args[1]; + sensor_bulk_reset(&mp->sb); mp->fifo_max = 0; mp->fifo_pkts_bytes = 0; - sched_add_timer(&mp->timer); + mp9250_reschedule_timer(mp); } -DECL_COMMAND(command_query_mpu9250, - "query_mpu9250 oid=%c clock=%u rest_ticks=%u"); +DECL_COMMAND(command_query_mpu9250, "query_mpu9250 oid=%c rest_ticks=%u"); void command_query_mpu9250_status(uint32_t *args) { struct mpu9250 *mp = oid_lookup(args[0], command_config_mpu9250); + + // Detect if a FIFO overrun occurred + uint8_t int_reg[] = {AR_INT_STATUS}; + uint8_t int_msg; + i2c_read(mp->i2c->i2c_config, sizeof(int_reg), int_reg, sizeof(int_msg), + &int_msg); + if (int_msg & FIFO_OVERFLOW_INT) + mp->sb.possible_overflows++; + + // Read latest FIFO count (with precise timing) + uint8_t reg[] = {AR_FIFO_COUNT_H}; uint8_t msg[2]; uint32_t time1 = timer_read_time(); - uint8_t reg[] = {AR_FIFO_COUNT_H}; i2c_read(mp->i2c->i2c_config, sizeof(reg), reg, sizeof(msg), msg); uint32_t time2 = timer_read_time(); - msg[0] = 0x1F & msg[0]; // discard 3 MSB - mp9250_status(mp, args[0], time1, time2, mp->fifo_pkts_bytes - / BYTES_PER_FIFO_ENTRY); + uint16_t fifo_bytes = ((msg[0] & 0x1f) << 8) | msg[1]; + + // Report status + sensor_bulk_status(&mp->sb, args[0], time1, time2-time1, fifo_bytes); } DECL_COMMAND(command_query_mpu9250_status, "query_mpu9250_status oid=%c"); @@ -269,15 +171,8 @@ mpu9250_task(void) struct mpu9250 *mp; foreach_oid(oid, mp, command_config_mpu9250) { uint_fast8_t flags = mp->flags; - if (!(flags & AX_PENDING)) { - continue; - } - if (flags & AX_HAVE_START) { - mp9250_start(mp, oid); - } - else { + if (flags & AX_PENDING) mp9250_query(mp, oid); - } } } DECL_TASK(mpu9250_task); diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig index c06bb6ffb..2ae90bee8 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -282,22 +282,24 @@ choice config STM32_FLASH_START_8000 bool "32KiB bootloader" if MACH_STM32F1 || MACH_STM32F2 || MACH_STM32F4 || MACH_STM32F7 config STM32_FLASH_START_8800 - bool "34KiB bootloader (Chitu v6 Bootloader)" if MACH_STM32F103 + bool "34KiB bootloader" if MACH_STM32F103 config STM32_FLASH_START_20200 - bool "128KiB bootloader with 512 byte offset (Prusa Buddy)" if MACH_STM32F4x5 + bool "128KiB bootloader with 512 byte offset" if MACH_STM32F4x5 + config STM32_FLASH_START_9000 + bool "36KiB bootloader" if MACH_STM32F1 config STM32_FLASH_START_C000 - bool "48KiB bootloader (MKS Robin Nano V3)" if MACH_STM32F4x5 + bool "48KiB bootloader" if MACH_STM32F4x5 config STM32_FLASH_START_10000 bool "64KiB bootloader" if MACH_STM32F103 || MACH_STM32F4 config STM32_FLASH_START_800 - bool "2KiB bootloader (HID Bootloader)" if MACH_STM32F103 + bool "2KiB bootloader" if MACH_STM32F103 config STM32_FLASH_START_1000 bool "4KiB bootloader" if MACH_STM32F1 || MACH_STM32F0 config STM32_FLASH_START_4000 - bool "16KiB bootloader (HID Bootloader)" if MACH_STM32F207 || MACH_STM32F401 || MACH_STM32F4x5 || MACH_STM32F103 || MACH_STM32F072 + bool "16KiB bootloader" if MACH_STM32F207 || MACH_STM32F401 || MACH_STM32F4x5 || MACH_STM32F103 || MACH_STM32F072 config STM32_FLASH_START_20000 - bool "128KiB bootloader (SKR SE BX v2.0)" if MACH_STM32H743 || MACH_STM32H723 || MACH_STM32F7 + bool "128KiB bootloader" if MACH_STM32H743 || MACH_STM32H723 || MACH_STM32F7 config STM32_FLASH_START_0000 bool "No bootloader" @@ -312,6 +314,7 @@ config FLASH_APPLICATION_ADDRESS default 0x8007000 if STM32_FLASH_START_7000 default 0x8008000 if STM32_FLASH_START_8000 default 0x8008800 if STM32_FLASH_START_8800 + default 0x8009000 if STM32_FLASH_START_9000 default 0x800C000 if STM32_FLASH_START_C000 default 0x8010000 if STM32_FLASH_START_10000 default 0x8020000 if STM32_FLASH_START_20000 diff --git a/src/stm32/fdcan.c b/src/stm32/fdcan.c index a1624f8c1..b0e8c01d1 100644 --- a/src/stm32/fdcan.c +++ b/src/stm32/fdcan.c @@ -162,10 +162,10 @@ canhw_set_filter(uint32_t id) can_filter(1, id); can_filter(2, id + 1); -#if CONFIG_MACH_STM32G0 +#if CONFIG_MACH_STM32G0 || CONFIG_MACH_STM32G4 SOC_CAN->RXGFC = ((id ? 3 : 1) << FDCAN_RXGFC_LSS_Pos | 0x02 << FDCAN_RXGFC_ANFS_Pos); -#elif CONFIG_MACH_STM32H7 || CONFIG_MAC_STM32G4 +#elif CONFIG_MACH_STM32H7 uint32_t flssa = (uint32_t)MSG_RAM.FLS - SRAMCAN_BASE; SOC_CAN->SIDFC = flssa | ((id ? 3 : 1) << FDCAN_SIDFC_LSS_Pos); SOC_CAN->GFC = 0x02 << FDCAN_GFC_ANFS_Pos; @@ -293,7 +293,7 @@ can_init(void) SOC_CAN->NBTP = btr; -#if CONFIG_MACH_STM32H7 || CONFIG_MAC_STM32G4 +#if CONFIG_MACH_STM32H7 /* Setup message RAM addresses */ uint32_t f0sa = (uint32_t)MSG_RAM.RXF0 - SRAMCAN_BASE; SOC_CAN->RXF0C = f0sa | (ARRAY_SIZE(MSG_RAM.RXF0) << FDCAN_RXF0C_F0S_Pos); diff --git a/src/stm32/stm32g0.c b/src/stm32/stm32g0.c index 7408612ad..819a5edd4 100644 --- a/src/stm32/stm32g0.c +++ b/src/stm32/stm32g0.c @@ -162,6 +162,8 @@ bootloader_request(void) void armcm_main(void) { + // Disable internal pull-down resistors on UCPDx CCx pins + SYSCFG->CFGR1 |= (SYSCFG_CFGR1_UCPD1_STROBE | SYSCFG_CFGR1_UCPD2_STROBE); SCB->VTOR = (uint32_t)VectorTable; // Reset clock registers (in case bootloader has changed them) diff --git a/src/stm32/stm32g4.c b/src/stm32/stm32g4.c index aed9ed8fa..139ea8eaa 100644 --- a/src/stm32/stm32g4.c +++ b/src/stm32/stm32g4.c @@ -105,6 +105,9 @@ enable_clock_stm32g4(void) enable_pclock(CRS_BASE); CRS->CR |= CRS_CR_AUTOTRIMEN | CRS_CR_CEN; } + + // Use PCLK for FDCAN + RCC->CCIPR = 2 << RCC_CCIPR_FDCANSEL_Pos; } // Main clock setup called at chip startup diff --git a/src/stm32/stm32h7_adc.c b/src/stm32/stm32h7_adc.c index 57d4b15c7..e9dc8f845 100644 --- a/src/stm32/stm32h7_adc.c +++ b/src/stm32/stm32h7_adc.c @@ -240,9 +240,10 @@ gpio_adc_setup(uint32_t pin) // Enable ADC adc->ISR = ADC_ISR_ADRDY; adc->ISR; // Dummy read to make sure write is flushed - adc->CR |= ADC_CR_ADEN; + while (!(adc->CR & ADC_CR_ADEN)) + adc->CR |= ADC_CR_ADEN; while (!(adc->ISR & ADC_ISR_ADRDY)) - ; + ; // Set ADC clock cycles sample time for every channel uint32_t av = (aticks | (aticks << 3) | (aticks << 6) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index ad2e7b3eb..5385c956c 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -15,7 +15,7 @@ #include "internal.h" // GPIO #include "sched.h" // DECL_INIT -#if CONFIG_MACH_STM32F1 || CONFIG_MACH_STM32G4 +#if CONFIG_MACH_STM32F1 // Transfer memory is accessed with 32bits, but contains only 16bits of data typedef volatile uint32_t epmword_t; #define WSIZE 2 @@ -25,6 +25,11 @@ 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; diff --git a/test/klippy/danger_options.cfg b/test/klippy/danger_options.cfg index 84bfd7c29..d7c1ad731 100644 --- a/test/klippy/danger_options.cfg +++ b/test/klippy/danger_options.cfg @@ -8,6 +8,7 @@ log_bed_mesh_at_startup: False log_shutdown_info: False allow_plugin_override: True multi_mcu_trsync_timeout: 0.05 +adc_ignore_limits: True [stepper_x] step_pin: PF0 diff --git a/test/klippy/printers.test b/test/klippy/printers.test index 4d5e0929a..7f6753508 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -63,12 +63,16 @@ DICTIONARY stm32f103-serial.dict # Printers using the stm32f401 DICTIONARY stm32f401.dict +CONFIG ../../config/printer-creality-ender5-s1-2023.cfg # Printers using the stm32f405 DICTIONARY stm32f405.dict # Printers using the stm32f407 DICTIONARY stm32f407.dict +CONFIG ../../config/generic-I3DBEEZ9.cfg + + # Printers using the stm32f429 DICTIONARY stm32f429.dict diff --git a/test/klippy/pwm.cfg b/test/klippy/pwm.cfg index fbda91269..af5b5b10e 100644 --- a/test/klippy/pwm.cfg +++ b/test/klippy/pwm.cfg @@ -5,6 +5,12 @@ value: 0 shutdown_value: 0 cycle_time: 0.01 +[pwm_cycle_time cycle_pwm_pin] +pin: PH7 +value: 0 +shutdown_value: 0 +cycle_time: 0.01 + [output_pin hard_pwm_pin] pin: PH6 pwm: True diff --git a/test/klippy/pwm.test b/test/klippy/pwm.test index 5e74a3e05..fdbf42f2a 100644 --- a/test/klippy/pwm.test +++ b/test/klippy/pwm.test @@ -16,18 +16,24 @@ SET_PIN PIN=soft_pwm_pin VALUE=0 SET_PIN PIN=soft_pwm_pin VALUE=0.5 SET_PIN PIN=soft_pwm_pin VALUE=1 +# Soft PWM with dynamic cycle time +# Test basic on off +SET_PIN PIN=cycle_pwm_pin VALUE=0 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=1 + # Test cycle time -SET_PIN PIN=soft_pwm_pin VALUE=0 CYCLE_TIME=0.1 -SET_PIN PIN=soft_pwm_pin VALUE=1 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.001 -SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.01 -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=1 +SET_PIN PIN=cycle_pwm_pin VALUE=0 CYCLE_TIME=0.1 +SET_PIN PIN=cycle_pwm_pin VALUE=1 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=0.001 +SET_PIN PIN=cycle_pwm_pin VALUE=0.75 CYCLE_TIME=0.01 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=1 # Test duplicate values -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.5 -SET_PIN PIN=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.75 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.5 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.75 CYCLE_TIME=0.5 +SET_PIN PIN=cycle_pwm_pin VALUE=0.75 CYCLE_TIME=0.75 # PWM tool # Basic test