Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ads1x1x: added support for ADC chip #6584

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

korsarNek
Copy link

Added support for ADC chips ADS1103, ADS1104, ADS1105, ADS1113, ADS1114 and ADS1115.

The PR is rougly based on #5618 but with a different architecture.

I've tested it with an ADS1115 and a linux mcu host.

@KevinOConnor
Copy link
Collaborator

Thanks. As high-level feedback, this sounds like useful functionality. I have some comments and questions:

  1. I'm confused why this new module ties into the temperature system (and is described in the temperature section of the documentation). I would have thought that one could add a new adc pin via the setup_pin() mechanism, and then users could define temperatures sensors (or adc buttons) using the existing my_ads_chip:pin1 mechanism. That is, wouldn't the generic adc pin system be able to handle existing thermistors, analog buttons, and other features?
  2. How does this module relate to (if at all) the ads1220 code proposed in Bulk ADC Sensors #6555 and the ads1118 code proposed in Support for Flashforge Creator Pro2 #6553?

-Kevin

@KevinOConnor KevinOConnor added the pending feedback Topic is pending feedback from submitter label May 14, 2024
@KevinOConnor
Copy link
Collaborator

I should also note that most temperature sensors in Klipper today are implemented with mcu code that can check the min/max temperature setting and directly invoke a shutdown if the range is exceeded. This code would be a different in that only the non-realtime host python code would check the min/max temperature range. I don't think that is a "blocker" but something to be aware of.

-Kevin

@korsarNek
Copy link
Author

korsarNek commented May 14, 2024

  1. Users can define temperature sensors with the my_ads_chip:pin1 mechanism. I have one definition for the chip in general that controls stuff like the i2c definition and then there is a separate definition for temperature sensors where the sensor_pin and sensor_type get used with the option to add a couple extra config values. The chip object also provide a setup_pin function. I thought that is the same system as the other temperature sensors.
    The separation of chip, sensor and pin and not including it all in the sensor is one of the things I've changed compared to the original PR where everything was defined on the sensor directly. One ADS chip can support up to 4 input pins, so 4 thermistors.
    Do you maybe have an example where I can see how it is supposed to look like?
  2. The ADS1118 is very similar to the ADS chips I've implemented here and seems support the same state machine, the big difference would just be that SPI is used instead of I²C. It could make sense to try to unify them.
    Concerning the ADS1220, from skimming through the data sheet, it looks like there are too many differences between that and the other ADS chips to reasonably try to unify them.

@korsarNek
Copy link
Author

I'll check the min max temperature part.

@korsarNek
Copy link
Author

Actually, the ADS1118 has a similar format, but supports a couple different commands. Maybe they are also different enough that it wouldn't make too much sense sharing the code.

@KevinOConnor
Copy link
Collaborator

KevinOConnor commented May 14, 2024

I have one definition for the chip in general that controls stuff like the i2c definition and then there is a separate definition for temperature sensors where the sensor_pin and sensor_type get used with the option to add a couple extra config values.

Can you elaborate on when/why a user would use sensor_type: ads1x1x instead of just using sensor_pin: my_ads1x1x:AIN0 with an existing sensor_type?

Do you maybe have an example where I can see how it is supposed to look like?

I don't know of a "one-to-one" example. You can look at klippy/extras/adc_scaled.py and config/generic-duet2-maestro.cfg for an example of a pseudo-adc chip. You can look at klippy/extras/sx1509.py and config/generic-duet2-duex.cfg for an example of an i2c digital gpio expander chip.

-Kevin

@KevinOConnor
Copy link
Collaborator

Also, as recently discussed in #6553, src/thermocouple.c and klippy/extras/spi_temperature.py could also be looked at as an example - though as mentioned there it may not be the best example.

-Kevin

@korsarNek
Copy link
Author

[ads1x1x adc]
chip: ADS1115
pga: 6.144V
samples_per_second: 16
i2c_mcu: host
i2c_bus: i2c.1
address_pin: GND
adc_voltage: 5

[temperature_fan chamber_fan]
pin: host:gpio17
min_temp: 0
max_temp: 80
max_power: 0.8
off_below: 0.1
shutdown_speed: 0
control: watermark
sensor_type: ads1x1x
sensor_pin: adc:AIN0
probe_type: Generic 3950
gcode_id: C
target_temp: 0
voltage_offset: -0.2
pullup_resistor: 4700

That's the config I'm using for my setup. There are sensors like HTU21D, where this would not be compatible with, they define their own communication protocol and would be incompatible with the ADS just reading out the voltage on the pin. I thought that was what you originally meant with sensor_type, but there are other sensor_types which are equivalent to the probe_type here. I guess I could have a look in how to make the thermistor and adc_temperature objects talk with the ads chip object to do the voltage to temperature conversion, then the probe_type property would not be necessary anymore and would become the sensor_type instead.

@KevinOConnor
Copy link
Collaborator

I'm not sure I understand.

To take a step back, on one of my printers, I have the following:

[heater_bed]
heater_pin: PC9
sensor_type: NTC 100K MGB18-104F39050L32
sensor_pin: PC4
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 57.006
pid_Ki: 3.334
pid_Kd: 243.700

I guess my question is, if I had an ads1115, could I just do:

[ads1x1x my_adc]
chip: ADS1115
pga: 6.144V
samples_per_second: 16
i2c_mcu: host
i2c_bus: i2c.1
address_pin: GND
adc_voltage: 5

[heater_bed]
heater_pin: PC9
sensor_type: NTC 100K MGB18-104F39050L32
sensor_pin: my_adc:AIN0
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 57.006
pid_Ki: 3.334
pid_Kd: 243.700

That is, if setup_pin() is implemented and returns a value between 0.0 and 1.0, would the existing temperature code work as is?

-Kevin

@korsarNek
Copy link
Author

As the code currently is, that would not work, the pin is missing the setup_minmax function. Apparently that's needed for an adc pin. Additionally to that, I have the code for requesting the voltage currently on the sensor instead of the pin, I'll have to move some stuff around from the sensor to the pin.

A config that currently would work is this:

[ads1x1x my_adc]
chip: ADS1115
pga: 6.144V
samples_per_second: 16
i2c_mcu: host
i2c_bus: i2c.1
address_pin: GND
adc_voltage: 5

[heater_bed]
heater_pin: PC9
>sensor_type: ads1x1x<
>probe_type: NTC 100K MGB18-104F39050L32<
sensor_pin: my_adc:AIN0
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 57.006
pid_Ki: 3.334
pid_Kd: 243.700

When I move the code around instead of using probe_type, it should be possible to use the NTC for the sensor_type instead.

@KevinOConnor KevinOConnor removed the pending feedback Topic is pending feedback from submitter label May 18, 2024
Copy link

github-actions bot commented Jun 1, 2024

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:

  1. 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.
  2. 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.
  3. 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.

@jackwakefield
Copy link
Contributor

Thanks for your work on this! I'm running into a couple of issues.

The first is, when using all 4 ADC pins, the readings are jumping from one pin to another. For example, AIN0 might be 35c, and AIN2 50c, but then it might suddenly show both AIN0 and AIN2 as 50c, and then next AIN2 as 35c and AIN0 as 50c.

The second issue is (and it might be a wiring issue on my end), I'm seeing ADS1X1X: command queue is overflowing, dropping requests in klippy.log. I do have 2 ADS1115's, and can see one is noticebly slower than the other. One will begin reading temperatures immediately, while the other is showing 0c for several seconds, and then refreshes about 10x slower than the other ADS1115. Again this might be an issue on my end, and I'll let you know if I find anything.

@korsarNek
Copy link
Author

korsarNek commented Aug 25, 2024

@KevinOConnor Concerning the sensor setup, I have a question on how this could be solved.

image

The problem is, the pullup resistor is technically not part of the chip, when someone would use the thermistor, there has to be a little circuit that could theoretically be different every time. It would be unusual for the pull up resistor to be different at different parts of the machine, but the voltage offset might be different, if you want an accurate reading, a different length of the wire could result in a different voltage offset that someone might want to account for.

On the pin, I don't have access to that information. The pins provided by the chip object, can also only be really used in connection with the ads1x1x sensor object, at the moment, they are not general pins that can be reused in other contexts.

Is there a way to have the thermistor as the sensor_type under these circumstances? I don't see how I could make that work with the current architecture.

I'm not sure I understand.

To take a step back, on one of my printers, I have the following:

[heater_bed]
heater_pin: PC9
sensor_type: NTC 100K MGB18-104F39050L32
sensor_pin: PC4
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 57.006
pid_Ki: 3.334
pid_Kd: 243.700

I guess my question is, if I had an ads1115, could I just do:

[ads1x1x my_adc]
chip: ADS1115
pga: 6.144V
samples_per_second: 16
i2c_mcu: host
i2c_bus: i2c.1
address_pin: GND
adc_voltage: 5

[heater_bed]
heater_pin: PC9
sensor_type: NTC 100K MGB18-104F39050L32
sensor_pin: my_adc:AIN0
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 57.006
pid_Ki: 3.334
pid_Kd: 243.700

That is, if setup_pin() is implemented and returns a value between 0.0 and 1.0, would the existing temperature code work as is?

-Kevin

@KevinOConnor
Copy link
Collaborator

Ah, the simple way to handle that is to not implement voltage_offset nor adc_voltage in ads1x1x.py . The existing thermistor (and adc_temperature) code already handles all that logic.

So, something like:

[ads1x1x my_adc]
chip: ADS1115
i2c_mcu: host
i2c_bus: i2c.1

[heater_bed]
heater_pin: PC9
sensor_type: NTC 100K MGB18-104F39050L32
sensor_pin: my_adc:AIN0
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 57.006
pid_Ki: 3.334
pid_Kd: 243.700

Said another way, just invoke the adc callback with the raw adc value (eg, see mcu.py:_handle_analog_in_state()) and let the existing Klipper code convert that to a voltage/temperature.

-Kevin

@legend069
Copy link

will this PR include support for a ADC chips connected to the printer MCU?

korsarNek and others added 3 commits October 12, 2024 14:53
Added a temperature sensor configuration for ADS1103, ADS1104, ADS1105, ADS1113, ADS1114 and ADS1115 chips that can be used to add Analog to Digital Conversion capability to machines that don't have that on their own. Like Raspberry Pi's or if more analog input pins are needed than the chip provides like for RP2040. Generally they can be used for any analog input, but the typical use case is for temperature measurement. This code also has been written with temperature measurement in mind and not as a general ADC.

Signed-off-by: Konstantin Koch <korsarnek@gmail.com>
Signed-off-by: Jack Wakefield <jackwakefield@protonmail.com>
…d try to be more accurate on the sample timing

Signed-off-by: Konstantin Koch <korsarnek@gmail.com>
@korsarNek
Copy link
Author

will this PR include support for a ADC chips connected to the printer MCU?

@legend069 I did not test a setup with an ADC chip connected to the printer MCU. I have the I2C pins needed for this connected to my linux mcu. From my understanding, Klipper should abstract a proper printer MCU and a linux MCU enough that they should behave the same.

My setup:
Hintergrund

The setup that I think, you mean:
Hintergrund2

Both should be working fine.

What's a bit special about this implementation is that most sensors have their loop to check if the temperature is out of bounds on the mcu, this implementation has it only in the python code. It might not be quite as real time as other sensors.

I'm currently using the chip only to measure the chamber temperature, so that setup is sufficient for me.


For the final version the config will change a bit. I've already tested a new version that doesn't use sensor_type: ads1x1x anymore but allows using the normal thermistors. The pushes from yesterday were just a rebase, the code wasn't quite compatible with the most recent version of klipper. I'll probably finish it up next weekend and then push the newest version.

…r instead of a custom ads1x1x sensor.

Signed-off-by: Konstantin Koch <korsarnek@gmail.com>
@korsarNek
Copy link
Author

@KevinOConnor I have added a new version that can now use thermistors or adc_temperature as the sensor instead of having a custom ads1x1x sensor.

[ads1x1x adc]
chip: ADS1115
pga: 6.144V
samples_per_second: 16
i2c_mcu: host
i2c_bus: i2c.1
address_pin: GND

[adc_temperature Chamber Sensor]
temperature1: ...
voltage1: ...
...

[temperature_fan chamber_fan]
pin: host:gpio17
min_temp: 0
max_temp: 80
max_power: 0.8
off_below: 0.1
shutdown_speed: 0
control: watermark
sensor_type: Chamber Sensor
sensor_pin: adc:AIN0
gcode_id: C

Copy link
Collaborator

@KevinOConnor KevinOConnor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. In general it seems fine to me. I have a handful of minor comments and questions. See below.

-Kevin

ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115 are I2C based Analog to
Digital Converters that can be used for temperature sensors. They provide 4
analog input pins either as single line or as differential input.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be useful to add a warning about using the sensor for heaters - maybe something like:

Note: Use caution if using this sensor to control heaters. The heater min_temp and max_temp are only verified in the host and only if the host is running and operating normally. (ADC inputs directly connected to the micro-controller verify min_temp and max_temp within the micro-controller and do not require a working connection to the host.)

```
[ads1x1x my_ads1x1x]
chip: ADS1115
#pga: 6.144V
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the default is 4.096V then please use the comment #pga: 4.096V.

Comment on lines 4840 to 4849
#mode: single
# Default value is single. Turn off the chip after a single reading or keep
# it running. Turning off saves power but turning it back on will be slower.
# Options are single and continuous.
#samples_per_second: 128
# Default value is 128. The amount of samples that the ADC can provide per
# second. A lower value makes the samples more accurate, but it takes longer
# until a new value is available.
# ADS101X's support 128, 250, 490, 920, 1600, 2400, 3300.
# ADS111X's support 8, 16, 32, 64, 128, 250, 475, 860.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, why would a user ever configure these two settings?

Since the code knows the report_time from setup_adc_callback() could the code just choose an appropriate value?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone might want to change it for higher accuracy, how many samples_per_second you can get away with also depends on how many inputs of the chip are used as one sampling would have to be done after the other.

It would make more sense to try to calculate that automatically, someone would need to know too much about how klipper and the ADS chip works to choose a good value.

A report_time in setup_adc_callback of for example 0.010s, would be 100 samples per second. On a single input I would choose 128. If 4 inputs are used, I would need to choose 920 (128 X 4 = 512, next best is 920) for ADS101X and 860 for ADS111X, reducing the accuracy but fitting in the time frame. I would then output a warning if the sample_rate needs to be higher than the chip can support.

It's a bit of extra logic finding the next best value and counting the used inputs, but then I could kick out the samples_per_second.

Concerning the single or continous mode, there is a theoretical power saving on single mode. I just wanted to include it for completeness.

If I compute the sample_rate anyway, this could be hard coded to continous.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Just to be clear, my comments here are not "blockers" to merging.

As for "power savings" - I can't see anyone caring, so it sounds like continuous is a better default and one can use a code constant to make it possible to enable sleep mode.

As for auto-calculating wakeup times - I'll defer to you if that's something worthwhile to implement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now added a calcuation for the sample rate. I made single mode the default though, it turns out that the idle state that we check on to see if the sampling is done, only gets set on single mode, not on continuous mode.

Comment on lines 224 to 225
def _handle_ready(self):
self.reset_all_devices()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not valid to communicate with an mcu in the "klippy:ready" callback. That callback can't be used to call anything that could report an error (nor anything that can block, or run for a long time).

You could kick off a reactor callback from ready, but it sounds like you want to do this work in the connect callback.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I wasn't quite sure which one was the correct one for this.

Comment on lines 206 to 207
self._printer.register_event_handler("klippy:shutdown", \
self.reset_all_devices)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a shutdown, the mcus wont accept any i2c commands. Maybe I'm missing something, but registering this event handler looks odd.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove it, reset on initialize should suffice anyway.

Comment on lines 247 to 249
except Exception:
logging.exception("ADS1X1X: error while sampling")
return None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a little more explanation for this error handling? If I understand the code correctly, if there is some error in this interaction the code will just stop sending temperature updates to the higher-level code. That sounds like that could cause a subtle failure (the higher level code will just assume the temperature hasn't changed for potentially extended periods). I'm not sure what the best solution is, but perhaps invoking a shutdown should be considered.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could adjust it so that a failed sampling adds to the invalid_count and invokes a shutdown if it happens often enough in a row.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

Comment on lines 310 to 312
self._sample_timer = \
self._reactor.register_timer(self._timer)
self._reactor.update_timer(self._sample_timer, self._reactor.NOW)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, you can do these two things directly with reactor.register_timer(self._timer, reactor.NOW)

if sample is not None:
self._process_sample(sample)

return self._reactor.monotonic() + self.report_time
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd that this does not use eventtime + self.report_time. Rescheduling based on the current time will result in an ongoing drift in the period between readings.

else:
target_value = sample / ADS111X_RESOLUTION

if self.maxval > self.minval:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calling code should verify maxval > minval and always provide values - so this looks like an odd check.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I wasn't sure if it does for certain, so I just added it to be safe. I'll remove that part.

Signed-off-by: Konstantin Koch <korsarnek@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants