pwm_cycle_time: New module for output pins with dynamic cycle times

Remove support for changing the cycle time of pwm pins from the
output_pin module.  Use a new pwm_cycle_time module that supports
setting dynamic cycle times.  This simplifies the output_pin code and
low-level pin update code.

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor 2023-01-13 11:20:19 -05:00
parent 1baa45913f
commit fd2feff67d
9 changed files with 204 additions and 38 deletions

View File

@ -61,12 +61,10 @@ gcode:
# P is the tone duration, S the tone frequency. # P is the tone duration, S the tone frequency.
# The frequency won't be pitch perfect. # The frequency won't be pitch perfect.
[output_pin BEEPER_pin] [pwm_cycle_time BEEPER_pin]
pin: ar37 pin: ar37
# Beeper pin. This parameter must be provided. # Beeper pin. This parameter must be provided.
# ar37 is the default RAMPS/MKS pin. # ar37 is the default RAMPS/MKS pin.
pwm: True
# A piezo beeper needs a PWM signal, a DC buzzer doesn't.
value: 0 value: 0
# Silent at power on, set to 1 if active low. # Silent at power on, set to 1 if active low.
shutdown_value: 0 shutdown_value: 0

View File

@ -8,6 +8,11 @@ All dates in this document are approximate.
## Changes ## 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 20240123: The output_pin `maximum_mcu_duration` parameter is
deprecated. Use a [pwm_tool config section](Config_Reference.md#pwm_tool) deprecated. Use a [pwm_tool config section](Config_Reference.md#pwm_tool)
instead. The option will be removed in the near future. instead. The option will be removed in the near future.

View File

@ -3153,6 +3153,24 @@ pin:
# See the "output_pin" section for the definition of these parameters. # 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] ### [static_digital_output]
Statically configured digital output pins (one may define any number Statically configured digital output pins (one may define any number

View File

@ -839,17 +839,10 @@ The following command is available when an
enabled. enabled.
#### SET_PIN #### SET_PIN
`SET_PIN PIN=config_name VALUE=<value> [CYCLE_TIME=<cycle_time>]`: Set `SET_PIN PIN=config_name VALUE=<value>`: Set the pin to the given
the pin to the given output `VALUE`. VALUE should be 0 or 1 for output `VALUE`. VALUE should be 0 or 1 for "digital" output pins. For
"digital" output pins. For PWM pins, set to a value between 0.0 and PWM pins, set to a value between 0.0 and 1.0, or between 0.0 and
1.0, or between 0.0 and `scale` if a scale is configured in the `scale` if a scale is configured in the output_pin config section.
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).
### [palette2] ### [palette2]
@ -978,6 +971,21 @@ babystepping), and subtract if from the probe's z_offset. This acts
to take a frequently used babystepping value, and "make it permanent". to take a frequently used babystepping value, and "make it permanent".
Requires a `SAVE_CONFIG` to take effect. 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=<value> [CYCLE_TIME=<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] ### [query_adc]
The query_adc module is automatically loaded. The query_adc module is automatically loaded.

View File

@ -374,6 +374,13 @@ is defined):
template expansion, the PROBE (or similar) command must be run prior template expansion, the PROBE (or similar) command must be run prior
to the macro containing this reference. 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 ## quad_gantry_level
The following information is available in the `quad_gantry_level` object The following information is available in the `quad_gantry_level` object

View File

@ -1,6 +1,6 @@
# Code to configure miscellaneous chips # PWM and digital output pin handling
# #
# Copyright (C) 2017-2021 Kevin O'Connor <kevin@koconnor.net> # Copyright (C) 2017-2024 Kevin O'Connor <kevin@koconnor.net>
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
@ -21,11 +21,9 @@ class PrinterOutputPin:
hardware_pwm = config.getboolean('hardware_pwm', False) hardware_pwm = config.getboolean('hardware_pwm', False)
self.mcu_pin.setup_cycle_time(cycle_time, hardware_pwm) self.mcu_pin.setup_cycle_time(cycle_time, hardware_pwm)
self.scale = config.getfloat('scale', 1., above=0.) self.scale = config.getfloat('scale', 1., above=0.)
self.last_cycle_time = self.default_cycle_time = cycle_time
else: else:
self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin')) self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin'))
self.scale = 1. self.scale = 1.
self.last_cycle_time = self.default_cycle_time = 0.
self.last_print_time = 0. self.last_print_time = 0.
# Support mcu checking for maximum duration # Support mcu checking for maximum duration
self.reactor = self.printer.get_reactor() self.reactor = self.printer.get_reactor()
@ -58,32 +56,30 @@ class PrinterOutputPin:
desc=self.cmd_SET_PIN_help) desc=self.cmd_SET_PIN_help)
def get_status(self, eventtime): def get_status(self, eventtime):
return {'value': self.last_value} return {'value': self.last_value}
def _set_pin(self, print_time, value, cycle_time, is_resend=False): def _set_pin(self, print_time, value, is_resend=False):
if value == self.last_value and cycle_time == self.last_cycle_time: if value == self.last_value and not is_resend:
if not is_resend:
return return
print_time = max(print_time, self.last_print_time + PIN_MIN_TIME) print_time = max(print_time, self.last_print_time + PIN_MIN_TIME)
if self.is_pwm: if self.is_pwm:
self.mcu_pin.set_pwm(print_time, value, cycle_time) self.mcu_pin.set_pwm(print_time, value)
else: else:
self.mcu_pin.set_digital(print_time, value) self.mcu_pin.set_digital(print_time, value)
self.last_value = value self.last_value = value
self.last_cycle_time = cycle_time
self.last_print_time = print_time self.last_print_time = print_time
if self.resend_interval and self.resend_timer is None: if self.resend_interval and self.resend_timer is None:
self.resend_timer = self.reactor.register_timer( self.resend_timer = self.reactor.register_timer(
self._resend_current_val, self.reactor.NOW) self._resend_current_val, self.reactor.NOW)
cmd_SET_PIN_help = "Set the value of an output pin" cmd_SET_PIN_help = "Set the value of an output pin"
def cmd_SET_PIN(self, gcmd): def cmd_SET_PIN(self, gcmd):
# Read requested value
value = gcmd.get_float('VALUE', minval=0., maxval=self.scale) value = gcmd.get_float('VALUE', minval=0., maxval=self.scale)
value /= self.scale value /= self.scale
cycle_time = gcmd.get_float('CYCLE_TIME', self.default_cycle_time,
above=0., maxval=MAX_SCHEDULE_TIME)
if not self.is_pwm and value not in [0., 1.]: if not self.is_pwm and value not in [0., 1.]:
raise gcmd.error("Invalid pin value") raise gcmd.error("Invalid pin value")
# Obtain print_time and apply requested settings
toolhead = self.printer.lookup_object('toolhead') toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback( 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): def _resend_current_val(self, eventtime):
if self.last_value == self.shutdown_value: if self.last_value == self.shutdown_value:
@ -97,8 +93,7 @@ class PrinterOutputPin:
if time_diff > 0.: if time_diff > 0.:
# Reschedule for resend time # Reschedule for resend time
return systime + time_diff return systime + time_diff
self._set_pin(print_time + PIN_MIN_TIME, self._set_pin(print_time + PIN_MIN_TIME, self.last_value, True)
self.last_value, self.last_cycle_time, True)
return systime + self.resend_interval return systime + self.resend_interval
def load_config_prefix(config): def load_config_prefix(config):

View File

@ -0,0 +1,123 @@
# Handle pwm output pins with variable frequency
#
# Copyright (C) 2017-2023 Kevin O'Connor <kevin@koconnor.net>
#
# 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. - start_value
shutdown_value = 1. - shutdown_value
self._start_value = max(0., min(1., start_value))
self._shutdown_value = max(0., min(1., shutdown_value))
self._last_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., 1.]:
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. - value
v = int(max(0., min(1., 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.
cycle_time = config.getfloat('cycle_time', 0.100, above=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., above=0.)
self.last_value = config.getfloat(
'value', 0., minval=0., maxval=self.scale) / self.scale
self.shutdown_value = config.getfloat(
'shutdown_value', 0., minval=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., maxval=self.scale)
value /= self.scale
cycle_time = gcmd.get_float('CYCLE_TIME', self.default_cycle_time,
above=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)

View File

@ -5,6 +5,12 @@ value: 0
shutdown_value: 0 shutdown_value: 0
cycle_time: 0.01 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] [output_pin hard_pwm_pin]
pin: PH6 pin: PH6
pwm: True pwm: True

View File

@ -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=0.5
SET_PIN PIN=soft_pwm_pin VALUE=1 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 # Test cycle time
SET_PIN PIN=soft_pwm_pin VALUE=0 CYCLE_TIME=0.1 SET_PIN PIN=cycle_pwm_pin VALUE=0 CYCLE_TIME=0.1
SET_PIN PIN=soft_pwm_pin VALUE=1 CYCLE_TIME=0.5 SET_PIN PIN=cycle_pwm_pin VALUE=1 CYCLE_TIME=0.5
SET_PIN PIN=soft_pwm_pin VALUE=0.5 CYCLE_TIME=0.001 SET_PIN PIN=cycle_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=cycle_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.5 CYCLE_TIME=1
# Test duplicate values # Test duplicate values
SET_PIN PIN=soft_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=soft_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=soft_pwm_pin VALUE=0.75 CYCLE_TIME=0.5 SET_PIN PIN=cycle_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.75 CYCLE_TIME=0.75
# PWM tool # PWM tool
# Basic test # Basic test