Skip to content

Commit

Permalink
tla2518: implement support
Browse files Browse the repository at this point in the history
  • Loading branch information
dalegaard committed Dec 7, 2023
1 parent a997aa5 commit f8a25c9
Show file tree
Hide file tree
Showing 5 changed files with 342 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<name>`.

```
[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
Expand Down
180 changes: 180 additions & 0 deletions klippy/extras/tla2518.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# TLA2518 8-channel 12-bit ADC support
#
# Copyright (C) 2023 Lasse Dalegaard <[email protected]>
#
# 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)
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
129 changes: 129 additions & 0 deletions src/tla2518.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// TLA2518 querying support
//
// Copyright (C) 2023 Lasse Dalegaard <[email protected]>
//
// 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);

0 comments on commit f8a25c9

Please sign in to comment.