diff --git a/README.md b/README.md index 4be4b815a..460f87319 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Features merged into the master branch: - [temperature_mcu: add reference_voltage](https://github.com/DangerKlippers/danger-klipper/pull/99) ([klipper#5713](https://github.com/Klipper3d/klipper/pull/5713)) +- [tla2518 support](https://github.com/DangerKlippers/danger-klipper/pull/103) + 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/docs/Config_Reference.md b/docs/Config_Reference.md index 5f81f9ca8..eb9977a01 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -4716,6 +4716,36 @@ i2c_address: # above parameters. ``` +### [tla2518] + +Configure a TLA2518 8-channel 12-bit ADC. These can be used as `sensor pin`s +within `[temperature_sensor]` blocks. Any number of TLA2518 chips can be +interfaced. Each TLA2518 provides 8 analog inputs, named `adc0` through `adc7`. +The chip name will be `tla2518_`. + +``` +[tla2518 my_tla2518] +cs_pin: +# The pin corresponding to the TLA2518 chip select line. This pin +# will be set to low at the start of SPI messages and raised to high +# after the message completes. This parameter must be provided. +#spi_speed: +#spi_bus: +#spi_software_sclk_pin: +#spi_software_mosi_pin: +#spi_software_miso_pin: +# See the "common SPI settings" section for a description of the +# above parameters. +``` + +A configured TLA2518 can be used as a temperature sensor input, e.g.: +``` +[temperature_sensor chamber] +sensor_type: PT1000 +sensor_pin: tla2518_my_tla2518:adc3 +pullup_resistor: 2200 +``` + ### [samd_sercom] SAMD SERCOM configuration to specify which pins to use on a given diff --git a/klippy/extras/tla2518.py b/klippy/extras/tla2518.py new file mode 100644 index 000000000..0f4d38362 --- /dev/null +++ b/klippy/extras/tla2518.py @@ -0,0 +1,180 @@ +# TLA2518 8-channel 12-bit ADC support +# +# Copyright (C) 2023 Lasse Dalegaard +# +# This file may be distributed under the terms of the GNU GPLv3 license. +from . import bus + +TLA2518_CHANNEL_COUNT = 8 +REPORT_TIME = 0.1 +SAMPLE_TIME = 128 / 1e6 + + +class TLA2518: + def __init__(self, config): + self.printer = config.get_printer() + self.ppins = config.get_printer().load_object(config, "pins") + self.config_error = config.error + self.reactor = self.printer.get_reactor() + self.name = " ".join(config.get_name().split()[1:]) + self.spi = bus.MCU_SPI_from_config(config, 0, default_speed=5000000) + self.mcu = self.spi.get_mcu() + self.mcu.register_config_callback(self._build_config) + self.oid = self.mcu.create_oid() + self.mcu.register_response( + self._handle_response, "tla2518_result", self.oid + ) + self.printer.register_event_handler( + "klippy:connect", self._handle_connect + ) + + self.channels = [None for _ in range(TLA2518_CHANNEL_COUNT)] + + def _build_config(self): + channels = 0 + for channel in self.channels: + if channel is not None: + channels |= channel.bit + + self.adc_channel_mask = channels + if channels != 0: + self.mcu.add_config_cmd( + "config_tla2518 oid=%u spi_oid=%u channels=%u" + % (self.oid, self.spi.get_oid(), channels) + ) + + def _chip_reset(self): + self._reg_write(0x01, 0x1) # Write RST to GENERAL_CFG + + # Wait for reset + timeout = self.reactor.monotonic() + 5.0 + while True: + if self._reg_read(0x01) & 0x01 == 0: + break + now = self.reactor.monotonic() + if now > timeout: + msg = f"Timeout trying to reset TLA2518 {self.name}" + self.printer.invoke_shutdown(msg) + raise + self.reactor.pause(now + 0.1) + if self._reg_read(0x00) != 0x81: + msg = f"TLA2518 '{self.name}' could not be reset" + self.printer.invoke_shutdown(msg) + raise + + def _handle_connect(self): + if self.adc_channel_mask == 0: + return + + self._chip_reset() + # DATA_CFG: APPEND_STATUS = 01b, include channel ID + self._reg_write(0x2, 1 << 4) + # OSR_CFG: OSR_CFG = 0111b, 128 sample oversampling + self._reg_write(0x3, 0x7) + # SEQUENCE_CFG: SEQ_MODE=10b, on the fly mode + self._reg_write(0x10, 0b10) + clock = self.mcu.get_query_slot(self.oid) + cmd = self.mcu.lookup_command( + "query_tla2518 oid=%c clock=%u rest_ticks=%u sample_ticks=%u" + ) + cmd.send( + [ + self.oid, + clock, + self.mcu.seconds_to_clock(REPORT_TIME), + self.mcu.seconds_to_clock(SAMPLE_TIME), + ] + ) + + def _handle_response(self, params): + idx = params["channel"] + if idx >= len(self.channels): + return + channel = self.channels[idx] + if channel is not None: + channel._handle_response(params) + + def _reg_write(self, addr, value): + self.spi.spi_send([0x08, addr, value]) + + def _reg_read(self, addr): + params = self.spi.spi_transfer_with_preface( + [0x10, addr, 0x00], + [0x00, 0x00, 0x00], + ) + return params["response"][0] + + def _register_channel(self, channel, output): + orig = channel + if channel.startswith("adc"): + channel = channel[3:] + try: + channel = int(channel) + except ValueError: + raise self.config_error(f"invalid TLA2518 channel '{orig}'") + if channel < 0 or channel >= TLA2518_CHANNEL_COUNT: + raise self.config_error( + f"invalid TLA2518 channel '{orig}', valid range 0 to " + f"{TLA2518_CHANNEL_COUNT-1}" + ) + cur = self.channels[channel] + if cur is None: + cur = self.channels[channel] = TLA2518Channel(self, channel) + cur._register_handler(output) + return cur + + # Pins interface + + def setup_pin(self, type, params): + if type != "adc": + raise self.ppins.error( + "pin type %s not supported on TLA2518" % (type,) + ) + return TLA2518ADC(self, params) + + +class TLA2518Channel: + def __init__(self, chip, idx): + self.idx = idx + self.bit = 1 << idx + self.handlers = [] + + def _register_handler(self, output): + self.handlers.append(output) + + def _handle_response(self, params): + for handler in self.handlers: + handler._handle_response(params) + + +class TLA2518ADC: + def __init__(self, chip, params): + self.channel = chip._register_channel(params["pin"], self) + self.chip = chip + self._callback = None + + def _handle_response(self, params): + if self._callback: + read_time = self.chip.mcu.clock_to_print_time(params["clock"]) + self._callback(read_time, params["value"] / 65535) + + # MCU_adc interface + + def setup_adc_callback(self, _report_time, callback): + self._callback = callback + + def setup_minmax( + self, + sample_time, + sample_count, + minval=0.0, + maxval=1.0, + range_check_count=0, + ): + pass + + +def load_config_prefix(config): + obj = TLA2518(config) + chip_name = "tla2518_" + obj.name + obj.ppins.register_chip(chip_name, obj) diff --git a/src/Makefile b/src/Makefile index 8d771f9eb..7bd3893b2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,7 +15,7 @@ src-$(CONFIG_WANT_DISPLAYS) += lcd_st7920.c lcd_hd44780.c src-$(CONFIG_WANT_SOFTWARE_SPI) += spi_software.c src-$(CONFIG_WANT_SOFTWARE_I2C) += i2c_software.c sensors-src-$(CONFIG_HAVE_GPIO_SPI) := thermocouple.c sensor_adxl345.c \ - sensor_angle.c + sensor_angle.c tla2518.c src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y) diff --git a/src/tla2518.c b/src/tla2518.c new file mode 100644 index 000000000..e962ea2d1 --- /dev/null +++ b/src/tla2518.c @@ -0,0 +1,129 @@ +// TLA2518 querying support +// +// Copyright (C) 2023 Lasse Dalegaard +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "basecmd.h" // oid_alloc +#include "board/irq.h" // irq_disable +#include "board/misc.h" // timer_read_time +#include "command.h" // DECL_COMMAND +#include "sched.h" // DECL_TASK +#include "spicmds.h" // spidev_transfer + +struct tla2518_instance { + struct timer round_timer; + struct timer sample_timer; + + uint32_t rest_time; + uint32_t sample_time; + struct spidev_s *spi; + uint8_t channels; + + uint8_t flags; + + uint8_t missing_channels; +}; + +enum { + FLAG_ACTIVE = 1, + FLAG_ROUND_PENDING = 2, + FLAG_SAMPLE_PENDING = 3, +}; + +static struct task_wake tla2518_wake; + +static uint_fast8_t tla2518_round_event(struct timer *round_timer) { + struct tla2518_instance *inst = + container_of(round_timer, struct tla2518_instance, round_timer); + + inst->round_timer.waketime += inst->rest_time; + // No current sample round ongoing, start a new one + if (inst->flags == FLAG_ACTIVE && inst->missing_channels == 0) { + inst->flags |= FLAG_ROUND_PENDING; + sched_wake_task(&tla2518_wake); + } + return SF_RESCHEDULE; +} + +static uint_fast8_t tla2518_sample_event(struct timer *sample_timer) { + struct tla2518_instance *inst = + container_of(sample_timer, struct tla2518_instance, sample_timer); + + sched_wake_task(&tla2518_wake); + inst->flags |= FLAG_SAMPLE_PENDING; + return SF_DONE; +} + +void command_config_tla2518(uint32_t *args) { + struct tla2518_instance *inst = + oid_alloc(args[0], command_config_tla2518, sizeof(*inst)); + inst->round_timer.func = tla2518_round_event; + inst->sample_timer.func = tla2518_sample_event; + inst->spi = spidev_oid_lookup(args[1]); + inst->channels = args[2]; + inst->missing_channels = 0; + inst->flags = 0; +} +DECL_COMMAND(command_config_tla2518, + "config_tla2518 oid=%c spi_oid=%c channels=%c"); + +void command_query_tla2518(uint32_t *args) { + struct tla2518_instance *inst = oid_lookup(args[0], command_config_tla2518); + + sched_del_timer(&inst->round_timer); + inst->round_timer.waketime = args[1]; + inst->rest_time = args[2]; + inst->sample_time = args[3]; + inst->flags = inst->rest_time != 0 ? FLAG_ACTIVE : 0; + if (!inst->rest_time) + return; + sched_add_timer(&inst->round_timer); +} +DECL_COMMAND(command_query_tla2518, + "query_tla2518 oid=%c clock=%u rest_ticks=%u sample_ticks=%u"); + +void tla2518_task(void) { + if (!sched_check_wake(&tla2518_wake)) + return; + uint8_t oid; + struct tla2518_instance *inst; + foreach_oid(oid, inst, command_config_tla2518) { + int flags = inst->flags; + // No sampling pending, skip over this. + if (!(flags & (FLAG_SAMPLE_PENDING | FLAG_ROUND_PENDING))) + continue; + + // Unset any flags except active, as we'll handle it now + irq_disable(); + inst->flags &= FLAG_ACTIVE; + irq_enable(); + + uint8_t msg[3] = {0}; + if ((flags & FLAG_ROUND_PENDING) && (inst->missing_channels == 0)) { + // Start new round + inst->missing_channels = inst->channels; + uint8_t first_channel = __builtin_ctz(inst->missing_channels); + msg[0] = 0x80 | (first_channel << 3); + spidev_transfer(inst->spi, 3, sizeof(msg), msg); + inst->sample_timer.waketime = timer_read_time() + inst->sample_time; + sched_add_timer(&inst->sample_timer); + } else if (inst->missing_channels != 0 && (flags & FLAG_SAMPLE_PENDING)) { + // Currently in a round, grab next sample and start the new one + uint8_t channel = __builtin_ctz(inst->missing_channels); + inst->missing_channels &= ~(1 << channel); + + uint8_t next_channel = __builtin_ctz(inst->missing_channels); + msg[0] = 0x80 | (next_channel << 3); + spidev_transfer(inst->spi, 3, sizeof(msg), msg); + sendf("tla2518_result oid=%c clock=%u channel=%c value=%u", oid, + timer_read_time(), msg[2] >> 4, msg[0] << 8 | msg[1]); + + if (inst->missing_channels != 0) { + inst->sample_timer.waketime = timer_read_time() + inst->sample_time; + sched_add_timer(&inst->sample_timer); + } + } + } +} +DECL_TASK(tla2518_task);