klipper
klipper copied to clipboard
Dual Loop PID
The idea behind the following PR is to have a more accurate bed surface temperature
Usually users who have big and thick beds have an offset between the bed surface and the thermistor temperature located on the heater, also they usually need to wait some time for the bed surface to reach the thermal equilibrium with the heater.
This PR allow the user set a new control algo that uses two sensors, one for the bed surface and one for the heater.
This new algorithm uses two PID loops, one for controlling the heater and one for controlling the bed surface, it works using the minimum control values beetwen the both PID loops. The main target for the heater will be always the bed surface, the maximum heater temperature is set on the heater section.
Here we can see a more detailed simulation:

The temperature on the heater (orange curve) was set to not exceed 100ºC while the bed surface target was on 60ºC
And here we can see a practical situation on my printer, which have a bed size 350mmx350mmx8mm. In this situation the same parameters was set as in the simulation

Here is how I setup the sensors in my bed:

and here how my configuration looks like now:

First of all, you have to consider support for other types temperature sensors, like MAX31865. Second thing, code looks "dirty", see "sensor_type_arg_name='sensor_type', sensor_pin_arg_name='sensor_pin'", it looks more like hacks rather than something suitable for master branch. You should consider to rewrite it from scratch in more generic way.
Any tips how I can instantiate two equals sensors in same section? As they look for the same arguments in the section
Maybe as I'm not creating a new sensor, just putting more than one together, I should create a cluster of PrinterSensorGeneric and use it to get the temperature from each sensor set in the PrinterSensorGeneric class?
I am not Kevin, so I can't provide the "right" solution for your case, but from my point of view it would be more interesting not to change heater section. For example we already have interface to lookup heater in heaters.py
def lookup_heater(self, heater_name):
if heater_name not in self.heaters:
raise self.printer.config_error(
"Unknown heater '%s'" % (heater_name,))
We can provide the same interface for sensors
def lookup_sensor(self, sensor_name):
if sensor_name not in self.sensors:
raise self.printer.config_error(
"Unknown sensor '%s'" % (sensor_name,))
return self.sensors[sensor_name]
def setup_sensor(self, config):
if not self.have_load_sensors:
self.load_config(config)
sensor_type = config.get('sensor_type')
if sensor_type not in self.sensor_factories:
raise self.printer.config_error(
"Unknown temperature sensor '%s'" % (sensor_type,))
if sensor_type == 'NTC 100K beta 3950':
config.deprecate('sensor_type', 'NTC 100K beta 3950')
sensor = self.sensor_factories[sensor_type](config)
sensor_name = config.get_name().split()[-1]
if sensor_name in self.sensors:
raise config.error("Sensor %s already registered" % (sensor_name,))
# Create sensor
self.sensors[sensor_name] = sensor
return sensor
And for example just get it by it's name in our init method
self.secondary_sensor_name = config.get("secondary_sensor_name")
pheaters = self.printer.load_object(config, 'heaters')
self.secondary_sensor = pheaters.lookup_sensor(self.secondary_sensor_name)
self.secondary_sensor.setup_callback(self.temperature_callback)
in this case we will have to modify "setup_callback" method to work with more than one callback.
#---------------------------------------------------------------------------------
[temperature_sensor SecondaryBedSensor]
sensor_type: MAX31865
sensor_pin: max31865_cs
spi_bus: spi1
rtd_nominal_r: 1000
rtd_reference_r: 4300
rtd_num_of_wires: 2
min_temp: 0
max_temp: 120
#---------------------------------------------------------------------------------
[heater_bed]
heater_pin: heater0_pin
sensor_type: Generic 3950
sensor_pin: bed_sensor_pin
min_temp: 0
max_temp: 120
secondary_sensor_name: SecondaryBedSensor
Thank you, your example helped me. The main issue following your example is to extract the temperature information from the sensors objects, one option as you said is to change the setup callback to work with more than one callback, for doing that we must re-implement the setup_callback method it in each sensor. Another similar ideia is to expose the temperature variable in the objects, some sensors like BME280 already have this value exposed by "self.temp", but PrinterADCtoTemperature for example don't have Maybe for implementing one of these ideas would be a good approach create an abstract Sensor class inhereted by all sensors, like:
from abc import ABC, abstractmethod
class Sensor(ABC):
def __init__(self, config):
self.printer = config.get_printer()
self._temp = self.min_temp = self.max_temp = 0.0
self._callbacks = []
...
def get_temp(self):
return self._temp
@abstractmethod
def setup_minmax(self, min_temp, max_temp):
pass
def setup_callback(self, cb):
self._callbacks.append(cb)
Another idea based on yours, is to load the entire temperature_sensor intead just the sensor:
def setup_heater(self, config, gcode_id=None):
heater_name = config.get_name().split()[-1]
if heater_name in self.heaters:
raise config.error("Heater %s already registered" % (heater_name,))
# Setup 1st sensor
sensor = self.setup_sensor(config)
# Setup 2nd sensor
secondary_sensor_name = config.get('secondary_sensor_name', None)
if secondary_sensor_name is not None:
full_name = "temperature_sensor " + secondary_sensor_name
secondary_sensor = self.printer.lookup_object(full_name)
else:
secondary_sensor = None
# Create heater
self.heaters[heater_name] = heater = Heater(config, sensor, secondary_sensor)
self.register_sensor(config, heater, gcode_id)
self.available_heaters.append(config.get_name())
return heater
Also in the Heater class we would have a new sensor, because for now the main idea is to use with the heater bed, but it can be useful for a heated chamber for example.
This is very useful in principle. I was thinking about a different solution, with a thermistor touching the bed and mounted inside a cork (which insulates from air perfectly), used with a macro to determine the time for the print to actually start, but a dual PID with a thermistor on the bed itself is of course better if nothing is on top of the aluminium (I have glass).
- Do you get faster heating times compared to the normal single PID?
- Can you get rid of the overshoot I see in the screenshot?
Thanks. Certainly seems interesting, but as high-level feedback, I'm not sure this is a good candidate for the master Klipper branch. This change would add notable config complexity and it isn't clear to me that there would be a sizeable number of people that would perform the research and reconfiguration necessary to utilize it.
To be clear, it does seem interesting. You might want to consider opening a new topic on Klipper Discourse to gather feedback from other users, and to gather their testing feedback.
-Kevin
I'm not sure this is a good candidate for the master Klipper branch.
FWIW, my printer and also the higher end designs (Voron, Railcore etc) typically use silicon heating mats attached to a MIC aluminum base. E.g. I'm using between 6mm and 8mm MIC plates on my printers with 220V heating mats and a thermal power of around 0.9W per square centimeter. What I'm seeing:
- Fast heat up (2-3 minutes) on the heating mat thermistor
- Time-wise a huge delay on the printing surface until the temperature migrates through the MIC plate, the magnetic sheet and the spring steel
- Temperature-wise a delta of around 10°C (Bed target 80°C) between the heating mat thermistor and the actual printing surface.
- I have gone through various trials of placing the thermistor that controls the PID loop but the results were not satisfying
If my understanding of this PR is correct, it would considerably improve heating up times, since
- A much higher temperature is "pushed" into the system during heating up
- The delta between target temperature and actual printing-surface-temperature will be significantly reduced (This can be of course worked around by just setting the target higher, which I do today)
I think it is a very valuable approach and would be happy to see such an option in Klipper.
Edit: As for the potential user base:
- Folks being able to build and setup such printer should be able to deal with dialing it in.
- The user-base of such printer is probably already quite high and constantly growing
- "Consumer printers" with standard (PCB type) heat beds are anyway not affected since the design probably does not allow for this second thermistor (also the effect might not be as significant as with the MIC/magnetic sheet/spring steel setups)
@KevinOConnor as that would benefit many large printer with thick aluminum plates like the Voron V2 I am sure the community would find ways to get the research for the average user to a minimum.
BTW RRF has that features for years. Setting it up is quit heavy but user are able to manage it
; Monitors & Limits
M143 H0 P1 T0 A2 S130 C0 ; Regulate (A2) bed heater (H0) to have pad sensor (T0) below 110°C. Use Heater monitor 1 for it
M143 H0 P2 T0 A0 S135 C0 ; Fault (A0) bed heater (H0) if pad sensor (T0) exceeds 135°C. Use Heater monitor 2 for it
M143 H0 P0 S120 ; Set bed heater max temperature to 120°C, use implict monitor 0 which is implicitly configured for heater fault
M143 H1 S400 ; set temperature limit for heater 1 to 275C
Do not ask me what all that means I ask a RRF user in the German Voron community to provide it to me.
https://docs.duet3d.com/en/User_manual/Reference/Gcodes#m143-maximum-heater-temperature
I personally also experimented with using the external mounted thermistor as the control thermistor for the heater and the thermistor at the silicon mate only as a safe gate to get the mate not heating too much. But was not able to get that to work reliable without modifying Klipper.
so the PR would be really appreciated.
Thanks for the feedback.
FWIW, I also have a Voron2 with two thermistors monitoring bed temperature ( https://github.com/KevinOConnor/voron2-mods/blob/master/bed/bed.md ). So I understand what is being reported.
In case anyone is curious, I currently use the thermistor on the bed heater for the PID, my PRINT_START macro sets a high bed target temperature during initial heating, it uses TEMPERATURE_WAIT on the bed plate thermistor, and then lower the target temperature. This provides a heating curve similar to those shown at the top of this PR. As others have observed, during printing there is a reliable static offset between the heater thermistor and the bed plate thermistor.
My observation is that there are several different ways to handle this type of bed setup. Both with code changes and without code changes. I think more feedback (and more user test results) would be needed before it would make sense to merge a change into Klipper. I don't think enough people will see this PR to get to that point. I think Klipper Discourse is a better place for that discussion and for gathering feedback.
-Kevin
it uses TEMPERATURE_WAIT on the bed plate thermistor, and then lower the target temperature.
Just whipped this up into a mock-up and indeed it is quite close:

My observations (on this printer / no enclosure / 6mm MIC / 21°C ambient temperature):
- As the bed surface is not in the PID loop, the overshoot in quite considerable and takes quite some time to drop back to the target temperature
- The (stable delta) needs to be manually compensated
- Delta needs to be dialed in for various bed temperatures, i.e. the delta for 60°C target is around 6°C, for 100°C target it is nearly 24°C (granted: on a non enclosed printer, it hardly makes sense to print materials that need 100°C bed temperature. So just to illustrate the logic)
It is a solution but the PID controlled one has its advantages I would think.
As the bed surface is not in the PID loop, the overshoot in quite considerable and takes quite some time to drop back to the target temperature
FWIW, it's easy to tweak the TEMPERATURE_WAIT on the bed plate thermistor to avoid overshoot.
In case anyone is curious, here's my macro:
[gcode_macro start_print_abs]
gcode:
{% set BED_TEMP = 105 %}
{% set EXTRUDER_TEMP = 245 %}
{% set BED_HEAT = 115 %}
{% set WAIT_CHAMBER_TEMP = 43 %}
{% set WAIT_PLATE_TEMP = 95 %}
# Prep
G90 ; absolute positioning
# Warm chamber
M104 S1
M140 S{BED_HEAT} # Set bed to heat chamber
G1 X125 Y100 Z20 F800
M106 S255 # Turn on layer cooling fan to distribute heat
TEMPERATURE_WAIT sensor="temperature_sensor chamber" minimum={WAIT_CHAMBER_TEMP}
TEMPERATURE_WAIT sensor="temperature_sensor bed_plate" minimum={WAIT_PLATE_TEMP}
# Heat extruder
M106 S0
G1 X5 Y5 Z5 F4000
G1 Z0.25 F400
M140 S{BED_TEMP}
M109 S{EXTRUDER_TEMP} # Wait for extruder temp
M190 S{BED_TEMP} # Wait for bed temp
G92 E0
-Kevin
You are right. I mislead myself graphically. In fact my overshoot was only 4°C. This is what I have been using to play around:
[gcode_macro TEMP_TEST]
gcode:
{% set BED_TEMP = params.BED_TEMP|default(60)|float %}
# Exceed target temperature by 25% to heat-soak the bed
{% set soak_temp = BED_TEMP + BED_TEMP * 0.25|float %}
{ action_respond_info('Soak temp: ' ~ soak_temp) }
# Make sure not to exceed max bed_temperature (120°C)
{% if soak_temp > 120.0 %}
{% set soak_temp = 120.0 %}
{ action_respond_info('Soak temp capped at 120C') }
{% endif %}
# Wait for bed surface sensor to reach the soak temp +/- 0.2°C
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=120
TEMPERATURE_WAIT SENSOR="temperature_sensor bed_surface" MINIMUM={soak_temp - 0.2} MAXIMUM={soak_temp + 0.2}
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={BED_TEMP}
TEMPERATURE_WAIT SENSOR="temperature_sensor bed_surface" MINIMUM={BED_TEMP - 0.2} MAXIMUM={BED_TEMP + 0.2}
Edit: The safeguard against too high soak_temp is in fact unneeded. In an earlier version I was experimenting with setting SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=120 dynamically.
This is very useful in principle. I was thinking about a different solution, with a thermistor touching the bed and mounted inside a cork (which insulates from air perfectly), used with a macro to determine the time for the print to actually start, but a dual PID with a thermistor on the bed itself is of course better if nothing is on top of the aluminium (I have glass).
- Do you get faster heating times compared to the normal single PID?
Yes, it is noticeable, in my normal conditions my heater can heat up to 150ºc, so for example to reach 100ºc in the surface I can take the advantage from this extra heat. I don't have these numbers right now, but I can provide for you.
- Can you get rid of the overshoot I see in the screenshot?
Unfortunately not, my pid was tuned for 100ºc, so for lower temperature like 60ºc I have a overshoot around 1.5ºC, for a 100ºC target, my overshoot is around 0.1ºC
The main point of this PR is to get as close as possible the desired temperature need for printing some material, there is no need to know the offsets between the heater and surface, and that offset sometimes can change because some external factor, like the fans below the bed for heat soak.
After the tips from @The-Futur1st , the implementation got simpler, but I didn't update the documentation after the refactoring.
I didn't know about Klipper Discourse, I will start a discussing there to get the validation from more users.
Thanks you guys
For info: I use the PWM value to define when the bed has actually reached a stable temperature: even after the (only) thermistor reaches it, the PWM takes longer to decrease until it is constant, because there is still heat flowing up.
Of course given the nice macros, I'll change my approach, but I thought it was good to mention it.
Thank you for your contribution to Klipper. Unfortunately, a reviewer has not assigned themselves to this GitHub Pull Request. All Pull Requests are reviewed before merging, and a reviewer will need to volunteer. Further information is available at: https://www.klipper3d.org/CONTRIBUTING.html
There are some steps that you can take now:
- Perform a self-review of your Pull Request by following the steps at: https://www.klipper3d.org/CONTRIBUTING.html#what-to-expect-in-a-review If you have completed a self-review, be sure to state the results of that self-review explicitly in the Pull Request comments. A reviewer is more likely to participate if the bulk of a review has already been completed.
- Consider opening a topic on the Klipper Discourse server to discuss this work. The Discourse server is a good place to discuss development ideas and to engage users interested in testing. Reviewers are more likely to prioritize Pull Requests with an active community of users.
- Consider helping out reviewers by reviewing other Klipper Pull Requests. Taking the time to perform a careful and detailed review of others work is appreciated. Regular contributors are more likely to prioritize the contributions of other regular contributors.
Unfortunately, if a reviewer does not assign themselves to this GitHub Pull Request then it will be automatically closed. If this happens, then it is a good idea to move further discussion to the Klipper Discourse server. Reviewers can reach out on that forum to let you know if they are interested and when they are available.
Best regards, ~ Your friendly GitIssueBot
PS: I'm just an automated script, not a human being.
Unfortunately a reviewer has not assigned themselves to this GitHub Pull Request and it is therefore being closed. It is a good idea to move further discussion to the Klipper Discourse server. Reviewers can reach out on that forum to let you know if they are interested and when they are available.
Best regards, ~ Your friendly GitIssueBot
PS: I'm just an automated script, not a human being.
@rodrigo2019 did you at least do the self-review? did you ask in the Dicourse forum and in Discord whether someone could perform the review for you? it's a pity this feature gets lost.
@dewi-ny-je yes, I did a self-review, before the github actions message you can see my force-push updating the documentation e parts of the code. I also opened a topic in the discourse as suggested: here
And I didn't ask to anybody to review this PR, because of this part in the guideline:
Please do not "ping" any of the reviewers and please do not direct submissions at them. All of the reviewers monitor the forums and PRs, and will take on reviews when they have time to.
Thanks for working on this and sharing your results. I do think it is interesting. However, I don't think there is consensus on this feature at this time. So, I think it is best to track the conversation on Discourse instead of github.
-Kevin