aboutsummaryrefslogtreecommitdiffstats
path: root/klippy/extras/pwm_cycle_time.py
blob: d2152316ad5508e06331b8555a0eede3c355a2a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# Handle pwm output pins with variable frequency
#
# Copyright (C) 2017-2025  Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.


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 get_mcu(self):
        return self._mcu

    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
        # 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)
        max_duration = pin_params["chip"].max_nominal_duration()
        cycle_time = config.getfloat(
            "cycle_time", 0.100, above=0.0, maxval=max_duration
        )
        self.mcu_pin = MCU_pwm_cycle(
            pin_params, cycle_time, self.last_value, self.shutdown_value
        )
        self.last_cycle_time = self.default_cycle_time = cycle_time
        # 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
        min_sched_time = self.mcu_pin.get_mcu().min_schedule_time()
        print_time = max(print_time, self.last_print_time + min_sched_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
        max_duration = self.mcu_pin.get_mcu().max_nominal_duration()
        cycle_time = gcmd.get_float(
            "CYCLE_TIME", self.default_cycle_time, above=0.0, maxval=max_duration
        )
        # 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)