aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGareth Farrington <gareth@waves.ky>2024-06-02 22:33:45 -0700
committerKevin O'Connor <kevin@koconnor.net>2024-07-31 21:22:33 -0400
commit055f07c6389fdae1be0ecafbe5b114ab08813fdd (patch)
tree953584f9f6a557183ea4b76c481cd351cdd10d35
parentc0095812ff18687ed25ce0f1ed468ebed8f81cfe (diff)
downloadkutter-055f07c6389fdae1be0ecafbe5b114ab08813fdd.tar.gz
kutter-055f07c6389fdae1be0ecafbe5b114ab08813fdd.tar.xz
kutter-055f07c6389fdae1be0ecafbe5b114ab08813fdd.zip
ads1220: Add ADS1220 bulk sensor to load_cell
Add support for the ADS1220 as an alternative to HX71x that supports SPI and higher sample rates. Signed-off-by: Gareth Farrington <gareth@waves.ky>
-rw-r--r--docs/API_Server.md15
-rw-r--r--docs/Config_Reference.md34
-rw-r--r--klippy/extras/ads1220.py187
-rw-r--r--klippy/extras/load_cell.py2
-rw-r--r--src/Kconfig10
-rw-r--r--src/Makefile1
-rw-r--r--src/sensor_ads1220.c161
-rw-r--r--test/configs/ar100.config1
-rw-r--r--test/configs/stm32f042.config1
9 files changed, 411 insertions, 1 deletions
diff --git a/docs/API_Server.md b/docs/API_Server.md
index 36d67b1a..f29bbeba 100644
--- a/docs/API_Server.md
+++ b/docs/API_Server.md
@@ -379,6 +379,21 @@ and might return:
and might later produce asynchronous messages such as:
`{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}`
+### ads1220/dump_ads1220
+
+This endpoint is used to subscribe to raw ADS1220 ADC data.
+Obtaining these low-level ADC updates may be useful for diagnostic
+and debugging purposes. Using this endpoint may increase Klipper's
+system load.
+
+A request may look like:
+`{"id": 123, "method":"ads1220/dump_ads1220",
+"params": {"sensor": "load_cell", "response_template": {}}}`
+and might return:
+`{"id": 123,"result":{"header":["time","counts"]}}`
+and might later produce asynchronous messages such as:
+`{"params":{"data":[[3292.432935, 562534], [3292.4394937, 5625322]]}}`
+
### pause_resume/cancel
This endpoint is similar to running the "PRINT_CANCEL" G-Code command.
diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md
index 03b10da1..63779df9 100644
--- a/docs/Config_Reference.md
+++ b/docs/Config_Reference.md
@@ -4702,6 +4702,40 @@ dout_pin:
# This must match the wiring of the chip. The sample rate cannot be changed
# in software.
```
+
+### ADS1220
+The ADS1220 is a 24 bit ADC supporting up to a 2Khz sample rate configurable in
+software.
+```
+sensor_type: ads1220
+cs_pin:
+# The pin connected to the ADS1220 chip select line. This parameter must
+# be provided.
+#spi_speed: 512000
+# This chip supports 2 speeds: 256000 or 512000. The faster speed is only
+# enabled when one of the Turbo sample rates is used. The correct spi_speed
+# is selected based on the sample rate.
+#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.
+data_ready_pin:
+# Pin connected to the ADS1220 data ready line. This parameter must be
+# provided.
+#gain: 128
+# Valid gain values are 128, 64, 32, 16, 8, 4, 2, 1
+# The default is 128
+#sample_rate: 660
+# This chip supports two ranges of sample rates, Normal and Turbo. In turbo
+# mode the chips c internal clock runs twice as fast and the SPI communication
+# speed is also doubled.
+# Normal sample rates: 20, 45, 90, 175, 330, 600, 1000
+# Turbo sample rates: 40, 90, 180, 350, 660, 1200, 2000
+# The default is 660
+```
+
## Board specific hardware support
### [sx1509]
diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py
new file mode 100644
index 00000000..fba74181
--- /dev/null
+++ b/klippy/extras/ads1220.py
@@ -0,0 +1,187 @@
+# ADS1220 Support
+#
+# Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+import logging
+from . import bulk_sensor, bus
+
+#
+# Constants
+#
+BYTES_PER_SAMPLE = 4 # samples are 4 byte wide unsigned integers
+MAX_SAMPLES_PER_MESSAGE = bulk_sensor.MAX_BULK_MSG_SIZE // BYTES_PER_SAMPLE
+UPDATE_INTERVAL = 0.10
+RESET_CMD = 0x06
+START_SYNC_CMD = 0x08
+RREG_CMD = 0x20
+WREG_CMD = 0x40
+NOOP_CMD = 0x0
+RESET_STATE = bytearray([0x0, 0x0, 0x0, 0x0])
+
+# turn bytearrays into pretty hex strings: [0xff, 0x1]
+def hexify(byte_array):
+ return "[%s]" % (", ".join([hex(b) for b in byte_array]))
+
+
+class ADS1220():
+ def __init__(self, config):
+ self.printer = printer = config.get_printer()
+ self.name = config.get_name().split()[-1]
+ self.last_error_count = 0
+ self.consecutive_fails = 0
+ # Chip options
+ # Gain
+ self.gain_options = {'1': 0x0, '2': 0x1, '4': 0x2, '8': 0x3, '16': 0x4,
+ '32': 0x5, '64': 0x6, '128': 0x7}
+ self.gain = config.getchoice('gain', self.gain_options, default='128')
+ # Sample rate
+ self.sps_normal = {'20': 20, '45': 45, '90': 90, '175': 175,
+ '330': 330, '600': 600, '1000': 1000}
+ self.sps_turbo = {'40': 40, '90': 90, '180': 180, '350': 350,
+ '660': 660, '1200': 1200, '2000': 2000}
+ self.sps_options = self.sps_normal.copy()
+ self.sps_options.update(self.sps_turbo)
+ self.sps = config.getchoice('sps', self.sps_options, default='660')
+ self.is_turbo = str(self.sps) in self.sps_turbo
+ # SPI Setup
+ spi_speed = 512000 if self.is_turbo else 256000
+ self.spi = bus.MCU_SPI_from_config(config, 1, default_speed=spi_speed)
+ self.mcu = mcu = self.spi.get_mcu()
+ self.oid = mcu.create_oid()
+ # Data Ready (DRDY) Pin
+ drdy_pin = config.get('data_ready_pin')
+ ppins = printer.lookup_object('pins')
+ drdy_ppin = ppins.lookup_pin(drdy_pin)
+ self.data_ready_pin = drdy_ppin['pin']
+ drdy_pin_mcu = drdy_ppin['chip']
+ if drdy_pin_mcu != self.mcu:
+ raise config.error("ADS1220 config error: SPI communication and"
+ " data_ready_pin must be on the same MCU")
+ # Bulk Sensor Setup
+ self.bulk_queue = bulk_sensor.BulkDataQueue(self.mcu, oid=self.oid)
+ # Clock tracking
+ chip_smooth = self.sps * UPDATE_INTERVAL * 2
+ # Measurement conversion
+ self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "<i")
+ # Process messages in batches
+ self.batch_bulk = bulk_sensor.BatchBulkHelper(
+ self.printer, self._process_batch, self._start_measurements,
+ self._finish_measurements, UPDATE_INTERVAL)
+ # publish raw samples to the socket
+ self.batch_bulk.add_mux_endpoint("ads1220/dump_ads1220", "sensor",
+ self.name,
+ {'header': ('time', 'counts')})
+ # Command Configuration
+ mcu.add_config_cmd(
+ "config_ads1220 oid=%d spi_oid=%d data_ready_pin=%s"
+ % (self.oid, self.spi.get_oid(), self.data_ready_pin))
+ mcu.add_config_cmd("query_ads1220 oid=%d rest_ticks=0"
+ % (self.oid,), on_restart=True)
+ mcu.register_config_callback(self._build_config)
+ self.query_ads1220_cmd = None
+
+ def _build_config(self):
+ cmdqueue = self.spi.get_command_queue()
+ self.query_ads1220_cmd = self.mcu.lookup_command(
+ "query_ads1220 oid=%c rest_ticks=%u", cq=cmdqueue)
+ self.ffreader.setup_query_command("query_ads1220_status oid=%c",
+ oid=self.oid, cq=cmdqueue)
+
+ def get_mcu(self):
+ return self.mcu
+
+ def get_samples_per_second(self):
+ return self.sps
+
+ # returns a tuple of the minimum and maximum value of the sensor, used to
+ # detect if a data value is saturated
+ def get_range(self):
+ return -0x800000, 0x7FFFFF
+
+ # add_client interface, direct pass through to bulk_sensor API
+ def add_client(self, callback):
+ self.batch_bulk.add_client(callback)
+
+ # Measurement decoding
+ def _convert_samples(self, samples):
+ adc_factor = 1. / (1 << 23)
+ count = 0
+ for ptime, val in samples:
+ samples[count] = (round(ptime, 6), val, round(val * adc_factor, 9))
+ count += 1
+ del samples[count:]
+
+ # Start, stop, and process message batches
+ def _start_measurements(self):
+ self.last_error_count = 0
+ self.consecutive_fails = 0
+ # Start bulk reading
+ self.reset_chip()
+ self.setup_chip()
+ rest_ticks = self.mcu.seconds_to_clock(1. / (10. * self.sps))
+ self.query_ads1220_cmd.send([self.oid, rest_ticks])
+ logging.info("ADS1220 starting '%s' measurements", self.name)
+ # Initialize clock tracking
+ self.ffreader.note_start()
+
+ def _finish_measurements(self):
+ # don't use serial connection after shutdown
+ if self.printer.is_shutdown():
+ return
+ # Halt bulk reading
+ self.query_ads1220_cmd.send_wait_ack([self.oid, 0])
+ self.ffreader.note_end()
+ logging.info("ADS1220 finished '%s' measurements", self.name)
+
+ def _process_batch(self, eventtime):
+ samples = self.ffreader.pull_samples()
+ self._convert_samples(samples)
+ return {'data': samples, 'errors': self.last_error_count,
+ 'overflows': self.ffreader.get_last_overflows()}
+
+ def reset_chip(self):
+ # the reset command takes 50us to complete
+ self.send_command(RESET_CMD)
+ # read startup register state and validate
+ val = self.read_reg(0x0, 4)
+ if val != RESET_STATE:
+ raise self.printer.command_error(
+ "Invalid ads1220 reset state (got %s vs %s).\n"
+ "This is generally indicative of connection problems\n"
+ "(e.g. faulty wiring) or a faulty ADS1220 chip."
+ % (hexify(val), hexify(RESET_STATE)))
+
+ def setup_chip(self):
+ continuous = 0x1 # enable continuous conversions
+ mode = 0x2 if self.is_turbo else 0x0 # turbo mode
+ sps_list = self.sps_turbo if self.is_turbo else self.sps_normal
+ data_rate = list(sps_list.keys()).index(str(self.sps))
+ reg_values = [(self.gain << 1),
+ (data_rate << 5) | (mode << 3) | (continuous << 2)]
+ self.write_reg(0x0, reg_values)
+ # start measurements immediately
+ self.send_command(START_SYNC_CMD)
+
+ def read_reg(self, reg, byte_count):
+ read_command = [RREG_CMD | (reg << 2) | (byte_count - 1)]
+ read_command += [NOOP_CMD] * byte_count
+ params = self.spi.spi_transfer(read_command)
+ return bytearray(params['response'][1:])
+
+ def send_command(self, cmd):
+ self.spi.spi_send([cmd])
+
+ def write_reg(self, reg, register_bytes):
+ write_command = [WREG_CMD | (reg << 2) | (len(register_bytes) - 1)]
+ write_command.extend(register_bytes)
+ self.spi.spi_send(write_command)
+ stored_val = self.read_reg(reg, len(register_bytes))
+ if register_bytes != stored_val:
+ raise self.printer.command_error(
+ "Failed to set ADS1220 register [0x%x] to %s: got %s. "
+ "This may be a connection problem (e.g. faulty wiring)" % (
+ reg, hexify(register_bytes), hexify(stored_val)))
+
+
+ADS1220_SENSOR_TYPE = {"ads1220": ADS1220}
diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py
index 7210f230..f6725d18 100644
--- a/klippy/extras/load_cell.py
+++ b/klippy/extras/load_cell.py
@@ -4,6 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import hx71x
+from . import ads1220
# Printer class that controls a load cell
class LoadCell:
@@ -29,6 +30,7 @@ def load_config(config):
# Sensor types
sensors = {}
sensors.update(hx71x.HX71X_SENSOR_TYPES)
+ sensors.update(ads1220.ADS1220_SENSOR_TYPE)
sensor_class = config.getchoice('sensor_type', sensors)
return LoadCell(config, sensor_class(config))
diff --git a/src/Kconfig b/src/Kconfig
index 4fb5268a..1fdfe02c 100644
--- a/src/Kconfig
+++ b/src/Kconfig
@@ -112,6 +112,10 @@ config WANT_HX71X
bool
depends on WANT_GPIO_BITBANGING
default y
+config WANT_ADS1220
+ bool
+ depends on HAVE_GPIO_SPI
+ default y
config WANT_SOFTWARE_I2C
bool
depends on HAVE_GPIO && HAVE_GPIO_I2C
@@ -122,7 +126,8 @@ config WANT_SOFTWARE_SPI
default y
config NEED_SENSOR_BULK
bool
- depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X
+ depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X \
+ || WANT_ADS1220
default y
menu "Optional features (to reduce code size)"
depends on HAVE_LIMITED_CODE_SIZE
@@ -144,6 +149,9 @@ config WANT_LDC1612
config WANT_HX71X
bool "Support HX711 and HX717 ADC chips"
depends on WANT_GPIO_BITBANGING
+config WANT_ADS1220
+ bool "Support ADS 1220 ADC chip"
+ depends on HAVE_GPIO_SPI
config WANT_SOFTWARE_I2C
bool "Support software based I2C \"bit-banging\""
depends on HAVE_GPIO && HAVE_GPIO_I2C
diff --git a/src/Makefile b/src/Makefile
index 4a1d2436..86c7407e 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -21,4 +21,5 @@ src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y)
src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c
src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c
src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c
+src-$(CONFIG_WANT_ADS1220) += sensor_ads1220.c
src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c
diff --git a/src/sensor_ads1220.c b/src/sensor_ads1220.c
new file mode 100644
index 00000000..044980c7
--- /dev/null
+++ b/src/sensor_ads1220.c
@@ -0,0 +1,161 @@
+// Support for ADS1220 ADC Chip
+//
+// Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
+//
+// This file may be distributed under the terms of the GNU GPLv3 license.
+
+#include "board/irq.h" // irq_disable
+#include "board/gpio.h" // gpio_out_write
+#include "board/misc.h" // timer_read_time
+#include "basecmd.h" // oid_alloc
+#include "command.h" // DECL_COMMAND
+#include "sched.h" // sched_add_timer
+#include "sensor_bulk.h" // sensor_bulk_report
+#include "spicmds.h" // spidev_transfer
+#include <stdint.h>
+
+struct ads1220_adc {
+ struct timer timer;
+ uint32_t rest_ticks;
+ struct gpio_in data_ready;
+ struct spidev_s *spi;
+ uint8_t pending_flag, data_count;
+ struct sensor_bulk sb;
+};
+
+// Flag types
+enum {
+ FLAG_PENDING = 1 << 0
+};
+
+#define BYTES_PER_SAMPLE 4
+
+static struct task_wake wake_ads1220;
+
+/****************************************************************
+ * ADS1220 Sensor Support
+ ****************************************************************/
+
+int8_t
+ads1220_is_data_ready(struct ads1220_adc *ads1220) {
+ return gpio_in_read(ads1220->data_ready) == 0;
+}
+
+// Event handler that wakes wake_ads1220() periodically
+static uint_fast8_t
+ads1220_event(struct timer *timer)
+{
+ struct ads1220_adc *ads1220 = container_of(timer, struct ads1220_adc,
+ timer);
+ uint32_t rest_ticks = ads1220->rest_ticks;
+ if (ads1220->pending_flag) {
+ ads1220->sb.possible_overflows++;
+ rest_ticks *= 4;
+ } else if (ads1220_is_data_ready(ads1220)) {
+ ads1220->pending_flag = 1;
+ sched_wake_task(&wake_ads1220);
+ rest_ticks *= 8;
+ }
+ ads1220->timer.waketime += rest_ticks;
+ return SF_RESCHEDULE;
+}
+
+// Add a measurement to the buffer
+static void
+add_sample(struct ads1220_adc *ads1220, uint8_t oid, uint_fast32_t counts)
+{
+ ads1220->sb.data[ads1220->sb.data_count] = counts;
+ ads1220->sb.data[ads1220->sb.data_count + 1] = counts >> 8;
+ ads1220->sb.data[ads1220->sb.data_count + 2] = counts >> 16;
+ ads1220->sb.data[ads1220->sb.data_count + 3] = counts >> 24;
+ ads1220->sb.data_count += BYTES_PER_SAMPLE;
+
+ if ((ads1220->sb.data_count + BYTES_PER_SAMPLE) >
+ ARRAY_SIZE(ads1220->sb.data)) {
+ sensor_bulk_report(&ads1220->sb, oid);
+ }
+}
+
+// ADS1220 ADC query
+void
+ads1220_read_adc(struct ads1220_adc *ads1220, uint8_t oid)
+{
+ uint8_t msg[3] = {0, 0, 0};
+ spidev_transfer(ads1220->spi, 1, sizeof(msg), msg);
+ ads1220->pending_flag = 0;
+ barrier();
+
+ // create 24 bit int from bytes
+ int32_t counts = (msg[0] << 16) | (msg[1] << 8) | msg[2];
+
+ // extend 2's complement 24 bits to 32bits
+ if (counts & 0x800000)
+ counts |= 0xFF000000;
+
+ add_sample(ads1220, oid, counts);
+}
+
+// Create an ads1220 sensor
+void
+command_config_ads1220(uint32_t *args)
+{
+ struct ads1220_adc *ads1220 = oid_alloc(args[0]
+ , command_config_ads1220, sizeof(*ads1220));
+ ads1220->timer.func = ads1220_event;
+ ads1220->pending_flag = 0;
+ ads1220->spi = spidev_oid_lookup(args[1]);
+ ads1220->data_ready = gpio_in_setup(args[2], 0);
+}
+DECL_COMMAND(command_config_ads1220, "config_ads1220 oid=%c"
+ " spi_oid=%c data_ready_pin=%u");
+
+// start/stop capturing ADC data
+void
+command_query_ads1220(uint32_t *args)
+{
+ uint8_t oid = args[0];
+ struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220);
+ sched_del_timer(&ads1220->timer);
+ ads1220->pending_flag = 0;
+ ads1220->rest_ticks = args[1];
+ if (!ads1220->rest_ticks) {
+ // End measurements
+ return;
+ }
+ // Start new measurements
+ sensor_bulk_reset(&ads1220->sb);
+ irq_disable();
+ ads1220->timer.waketime = timer_read_time() + ads1220->rest_ticks;
+ sched_add_timer(&ads1220->timer);
+ irq_enable();
+}
+DECL_COMMAND(command_query_ads1220, "query_ads1220 oid=%c rest_ticks=%u");
+
+void
+command_query_ads1220_status(const uint32_t *args)
+{
+ uint8_t oid = args[0];
+ struct ads1220_adc *ads1220 = oid_lookup(oid, command_config_ads1220);
+ irq_disable();
+ const uint32_t start_t = timer_read_time();
+ uint8_t is_data_ready = ads1220_is_data_ready(ads1220);
+ irq_enable();
+ uint8_t pending_bytes = is_data_ready ? BYTES_PER_SAMPLE : 0;
+ sensor_bulk_status(&ads1220->sb, oid, start_t, 0, pending_bytes);
+}
+DECL_COMMAND(command_query_ads1220_status, "query_ads1220_status oid=%c");
+
+// Background task that performs measurements
+void
+ads1220_capture_task(void)
+{
+ if (!sched_check_wake(&wake_ads1220))
+ return;
+ uint8_t oid;
+ struct ads1220_adc *ads1220;
+ foreach_oid(oid, ads1220, command_config_ads1220) {
+ if (ads1220->pending_flag)
+ ads1220_read_adc(ads1220, oid);
+ }
+}
+DECL_TASK(ads1220_capture_task);
diff --git a/test/configs/ar100.config b/test/configs/ar100.config
index 61734ab9..a1335176 100644
--- a/test/configs/ar100.config
+++ b/test/configs/ar100.config
@@ -5,3 +5,4 @@ CONFIG_WANT_SOFTWARE_I2C=n
CONFIG_WANT_SOFTWARE_SPI=n
CONFIG_WANT_LIS2DW=n
CONFIG_WANT_HX71X=n
+CONFIG_WANT_ADS1220=n
diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config
index 3e0b2a55..53cf1281 100644
--- a/test/configs/stm32f042.config
+++ b/test/configs/stm32f042.config
@@ -5,3 +5,4 @@ CONFIG_WANT_SOFTWARE_I2C=n
CONFIG_WANT_LIS2DW=n
CONFIG_WANT_LDC1612=n
CONFIG_WANT_HX71X=n
+CONFIG_WANT_ADS1220=n