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

tla2518: implement support #103

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ If I want my printer to light itself on fire, I should be able to make my printe

- [homing: min_home_dist](https://github.com/DangerKlippers/danger-klipper/pull/90)

- [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 @@ -4796,6 +4796,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);