aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/API_Server.md15
-rw-r--r--docs/Config_Reference.md57
-rw-r--r--klippy/extras/hx71x.py170
-rw-r--r--klippy/extras/load_cell.py36
-rw-r--r--src/Kconfig9
-rw-r--r--src/Makefile1
-rw-r--r--src/sensor_hx71x.c245
-rw-r--r--test/configs/ar100.config1
-rw-r--r--test/configs/stm32f042.config1
9 files changed, 534 insertions, 1 deletions
diff --git a/docs/API_Server.md b/docs/API_Server.md
index cc0922e3..36d67b1a 100644
--- a/docs/API_Server.md
+++ b/docs/API_Server.md
@@ -364,6 +364,21 @@ and might later produce asynchronous messages such as:
The "header" field in the initial query response is used to describe
the fields found in later "data" responses.
+### hx71x/dump_hx71x
+
+This endpoint is used to subscribe to raw HX711 and HX717 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":"hx71x/dump_hx71x",
+"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 9be53dfa..03b10da1 100644
--- a/docs/Config_Reference.md
+++ b/docs/Config_Reference.md
@@ -4645,6 +4645,63 @@ adc2:
# above parameters.
```
+## Load Cells
+
+### [load_cell]
+Load Cell. Uses an ADC sensor attached to a load cell to create a digital
+scale.
+
+```
+[load_cell]
+sensor_type:
+# This must be one of the supported Sensor types, see `Load Cell Sensors`
+```
+
+### Load Cell Sensors
+
+Load Cell supports dedicated Analog to Digital Converter (ADC) chips that sample
+at a high data rate.
+
+#### XH711
+This is a 24 bit low sample rate chip using "bit-bang" communications. It is
+suitable for filament scales.
+```
+sensor_type: hx711
+sclk_pin:
+# The pin connected to the HX711 clock line. This parameter must be provided.
+dout_pin:
+# The pin connected to the HX711 data output line. This parameter must be
+# provided.
+#gain: A-128
+# Valid values for gain are: A-128, A-64, B-32. The default is A-128.
+# 'A' denotes the input channel and the number denotes the gain. Only the 3
+# listed combinations are supported by the chip. Note that changing the gain
+# setting also selects the channel being read.
+#sample_rate: 80
+# Valid values for sample_rate are 80 or 10. The default value is 80.
+# This must match the wiring of the chip. The sample rate cannot be changed
+# in software.
+```
+
+### HX717
+This is the 4x higher sample rate version of the HX711, suitable for probing.
+```
+sensor_type: hx717
+sclk_pin:
+# The pin connected to the HX717 clock line. This parameter must be provided.
+dout_pin:
+# The pin connected to the HX717 data output line. This parameter must be
+# provided.
+#gain: A-128
+# Valid values for gain are A-128, B-64, A-64, B-8.
+# 'A' denotes the input channel and the number denotes the gain setting.
+# Only the 4 listed combinations are supported by the chip. Note that
+# changing the gain setting also selects the channel being read.
+#sample_rate: 320
+# Valid values for sample_rate are: 10, 20, 80, 320. The default is 320.
+# This must match the wiring of the chip. The sample rate cannot be changed
+# in software.
+```
## Board specific hardware support
### [sx1509]
diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py
new file mode 100644
index 00000000..1b1128ac
--- /dev/null
+++ b/klippy/extras/hx71x.py
@@ -0,0 +1,170 @@
+# HX711/HX717 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
+
+#
+# Constants
+#
+UPDATE_INTERVAL = 0.10
+SAMPLE_ERROR_DESYNC = -0x80000000
+SAMPLE_ERROR_LONG_READ = 0x40000000
+
+# Implementation of HX711 and HX717
+class HX71xBase():
+ def __init__(self, config, sensor_type,
+ sample_rate_options, default_sample_rate,
+ gain_options, default_gain):
+ self.printer = printer = config.get_printer()
+ self.name = config.get_name().split()[-1]
+ self.last_error_count = 0
+ self.consecutive_fails = 0
+ self.sensor_type = sensor_type
+ # Chip options
+ dout_pin_name = config.get('dout_pin')
+ sclk_pin_name = config.get('sclk_pin')
+ ppins = printer.lookup_object('pins')
+ dout_ppin = ppins.lookup_pin(dout_pin_name)
+ sclk_ppin = ppins.lookup_pin(sclk_pin_name)
+ self.mcu = mcu = dout_ppin['chip']
+ self.oid = mcu.create_oid()
+ if sclk_ppin['chip'] is not mcu:
+ raise config.error("%s config error: All pins must be "
+ "connected to the same MCU" % (self.name,))
+ self.dout_pin = dout_ppin['pin']
+ self.sclk_pin = sclk_ppin['pin']
+ # Samples per second choices
+ self.sps = config.getchoice('sample_rate', sample_rate_options,
+ default=default_sample_rate)
+ # gain/channel choices
+ self.gain_channel = int(config.getchoice('gain', gain_options,
+ default=default_gain))
+ ## Bulk Sensor Setup
+ self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, oid=self.oid)
+ # Clock tracking
+ chip_smooth = self.sps * UPDATE_INTERVAL * 2
+ 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
+ dump_path = "%s/dump_%s" % (sensor_type, sensor_type)
+ self.batch_bulk.add_mux_endpoint(dump_path, "sensor", self.name,
+ {'header': ('time', 'counts')})
+ # Command Configuration
+ self.query_hx71x_cmd = None
+ mcu.add_config_cmd(
+ "config_hx71x oid=%d gain_channel=%d dout_pin=%s sclk_pin=%s"
+ % (self.oid, self.gain_channel, self.dout_pin, self.sclk_pin))
+ mcu.add_config_cmd("query_hx71x oid=%d rest_ticks=0"
+ % (self.oid,), on_restart=True)
+
+ mcu.register_config_callback(self._build_config)
+
+ def _build_config(self):
+ self.query_hx71x_cmd = self.mcu.lookup_command(
+ "query_hx71x oid=%c rest_ticks=%u")
+ self.ffreader.setup_query_command("query_hx71x_status oid=%c",
+ oid=self.oid,
+ cq=self.mcu.alloc_command_queue())
+
+ 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:
+ if val == SAMPLE_ERROR_DESYNC or val == SAMPLE_ERROR_LONG_READ:
+ self.last_error_count += 1
+ break # additional errors are duplicates
+ 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.consecutive_fails = 0
+ self.last_error_count = 0
+ # Start bulk reading
+ rest_ticks = self.mcu.seconds_to_clock(1. / (10. * self.sps))
+ self.query_hx71x_cmd.send([self.oid, rest_ticks])
+ logging.info("%s starting '%s' measurements",
+ self.sensor_type, 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_hx71x_cmd.send_wait_ack([self.oid, 0])
+ self.ffreader.note_end()
+ logging.info("%s finished '%s' measurements",
+ self.sensor_type, self.name)
+
+ def _process_batch(self, eventtime):
+ prev_overflows = self.ffreader.get_last_overflows()
+ prev_error_count = self.last_error_count
+ samples = self.ffreader.pull_samples()
+ self._convert_samples(samples)
+ overflows = self.ffreader.get_last_overflows() - prev_overflows
+ errors = self.last_error_count - prev_error_count
+ if errors > 0:
+ logging.error("%s: Forced sensor restart due to error", self.name)
+ self._finish_measurements()
+ self._start_measurements()
+ elif overflows > 0:
+ self.consecutive_fails += 1
+ if self.consecutive_fails > 4:
+ logging.error("%s: Forced sensor restart due to overflows",
+ self.name)
+ self._finish_measurements()
+ self._start_measurements()
+ else:
+ self.consecutive_fails = 0
+ return {'data': samples, 'errors': self.last_error_count,
+ 'overflows': self.ffreader.get_last_overflows()}
+
+
+class HX711(HX71xBase):
+ def __init__(self, config):
+ super(HX711, self).__init__(config, "hx711",
+ # HX711 sps options
+ {80: 80, 10: 10}, 80,
+ # HX711 gain/channel options
+ {'A-128': 1, 'B-32': 2, 'A-64': 3}, 'A-128')
+
+
+class HX717(HX71xBase):
+ def __init__(self, config):
+ super(HX717, self).__init__(config, "hx717",
+ # HX717 sps options
+ {320: 320, 80: 80, 20: 20, 10: 10}, 320,
+ # HX717 gain/channel options
+ {'A-128': 1, 'B-64': 2, 'A-64': 3,
+ 'B-8': 4}, 'A-128')
+
+
+HX71X_SENSOR_TYPES = {
+ "hx711": HX711,
+ "hx717": HX717
+}
diff --git a/klippy/extras/load_cell.py b/klippy/extras/load_cell.py
new file mode 100644
index 00000000..7210f230
--- /dev/null
+++ b/klippy/extras/load_cell.py
@@ -0,0 +1,36 @@
+# Load Cell Implementation
+#
+# Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+from . import hx71x
+
+# Printer class that controls a load cell
+class LoadCell:
+ def __init__(self, config, sensor):
+ self.printer = printer = config.get_printer()
+ self.sensor = sensor # must implement BulkAdcSensor
+ # startup, when klippy is ready, start capturing data
+ printer.register_event_handler("klippy:ready", self._handle_ready)
+
+ def _handle_ready(self):
+ self.sensor.add_client(self._on_sample)
+
+ def _on_sample(self, msg):
+ return True
+
+ def get_sensor(self):
+ return self.sensor
+
+ def get_status(self, eventtime):
+ return {}
+
+def load_config(config):
+ # Sensor types
+ sensors = {}
+ sensors.update(hx71x.HX71X_SENSOR_TYPES)
+ sensor_class = config.getchoice('sensor_type', sensors)
+ return LoadCell(config, sensor_class(config))
+
+def load_config_prefix(config):
+ return load_config(config)
diff --git a/src/Kconfig b/src/Kconfig
index 7dcea3ba..4fb5268a 100644
--- a/src/Kconfig
+++ b/src/Kconfig
@@ -108,6 +108,10 @@ config WANT_LDC1612
bool
depends on HAVE_GPIO_I2C
default y
+config WANT_HX71X
+ bool
+ depends on WANT_GPIO_BITBANGING
+ default y
config WANT_SOFTWARE_I2C
bool
depends on HAVE_GPIO && HAVE_GPIO_I2C
@@ -118,7 +122,7 @@ config WANT_SOFTWARE_SPI
default y
config NEED_SENSOR_BULK
bool
- depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612
+ depends on WANT_SENSORS || WANT_LIS2DW || WANT_LDC1612 || WANT_HX71X
default y
menu "Optional features (to reduce code size)"
depends on HAVE_LIMITED_CODE_SIZE
@@ -137,6 +141,9 @@ config WANT_LIS2DW
config WANT_LDC1612
bool "Support ldc1612 eddy current sensor"
depends on HAVE_GPIO_I2C
+config WANT_HX71X
+ bool "Support HX711 and HX717 ADC chips"
+ depends on WANT_GPIO_BITBANGING
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 ed98172e..4a1d2436 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -20,4 +20,5 @@ sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c
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_NEED_SENSOR_BULK) += sensor_bulk.c
diff --git a/src/sensor_hx71x.c b/src/sensor_hx71x.c
new file mode 100644
index 00000000..4f0a8c5b
--- /dev/null
+++ b/src/sensor_hx71x.c
@@ -0,0 +1,245 @@
+// Support for bit-banging commands to HX711 and HX717 ADC chips
+//
+// Copyright (C) 2024 Gareth Farrington <gareth@waves.ky>
+//
+// This file may be distributed under the terms of the GNU GPLv3 license.
+
+#include "autoconf.h" // CONFIG_MACH_AVR
+#include "board/gpio.h" // gpio_out_write
+#include "board/irq.h" // irq_poll
+#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 <stdbool.h>
+#include <stdint.h>
+
+struct hx71x_adc {
+ struct timer timer;
+ uint8_t gain_channel; // the gain+channel selection (1-4)
+ uint8_t pending_flag;
+ uint32_t rest_ticks;
+ uint32_t last_error;
+ struct gpio_in dout; // pin used to receive data from the hx71x
+ struct gpio_out sclk; // pin used to generate clock for the hx71x
+ struct sensor_bulk sb;
+};
+
+#define BYTES_PER_SAMPLE 4
+#define SAMPLE_ERROR_DESYNC 1 << 31
+#define SAMPLE_ERROR_READ_TOO_LONG 1 << 30
+
+static struct task_wake wake_hx71x;
+
+
+/****************************************************************
+ * Low-level bit-banging
+ ****************************************************************/
+
+#define MIN_PULSE_TIME nsecs_to_ticks(200)
+
+static uint32_t
+nsecs_to_ticks(uint32_t ns)
+{
+ return timer_from_us(ns * 1000) / 1000000;
+}
+
+// Pause for 200ns
+static void
+hx71x_delay_noirq(void)
+{
+ if (CONFIG_MACH_AVR) {
+ // Optimize avr, as calculating time takes longer than needed delay
+ asm("nop\n nop");
+ return;
+ }
+ uint32_t end = timer_read_time() + MIN_PULSE_TIME;
+ while (timer_is_before(timer_read_time(), end))
+ ;
+}
+
+// Pause for a minimum of 200ns
+static void
+hx71x_delay(void)
+{
+ if (CONFIG_MACH_AVR)
+ // Optimize avr, as calculating time takes longer than needed delay
+ return;
+ uint32_t end = timer_read_time() + MIN_PULSE_TIME;
+ while (timer_is_before(timer_read_time(), end))
+ irq_poll();
+}
+
+// Read 'num_bits' from the sensor
+static uint32_t
+hx71x_raw_read(struct gpio_in dout, struct gpio_out sclk, int num_bits)
+{
+ uint32_t bits_read = 0;
+ while (num_bits--) {
+ irq_disable();
+ gpio_out_toggle_noirq(sclk);
+ hx71x_delay_noirq();
+ gpio_out_toggle_noirq(sclk);
+ uint_fast8_t bit = gpio_in_read(dout);
+ irq_enable();
+ hx71x_delay();
+ bits_read = (bits_read << 1) | bit;
+ }
+ return bits_read;
+}
+
+
+/****************************************************************
+ * HX711 and HX717 Sensor Support
+ ****************************************************************/
+
+// Check if data is ready
+static uint_fast8_t
+hx71x_is_data_ready(struct hx71x_adc *hx71x)
+{
+ return !gpio_in_read(hx71x->dout);
+}
+
+// Event handler that wakes wake_hx71x() periodically
+static uint_fast8_t
+hx71x_event(struct timer *timer)
+{
+ struct hx71x_adc *hx71x = container_of(timer, struct hx71x_adc, timer);
+ uint32_t rest_ticks = hx71x->rest_ticks;
+ if (hx71x->pending_flag) {
+ hx71x->sb.possible_overflows++;
+ rest_ticks *= 4;
+ } else if (hx71x_is_data_ready(hx71x)) {
+ // New sample pending
+ hx71x->pending_flag = 1;
+ sched_wake_task(&wake_hx71x);
+ rest_ticks *= 8;
+ }
+ hx71x->timer.waketime += rest_ticks;
+ return SF_RESCHEDULE;
+}
+
+static void
+add_sample(struct hx71x_adc *hx71x, uint8_t oid, uint32_t counts,
+ uint8_t force_flush) {
+ // Add measurement to buffer
+ hx71x->sb.data[hx71x->sb.data_count] = counts;
+ hx71x->sb.data[hx71x->sb.data_count + 1] = counts >> 8;
+ hx71x->sb.data[hx71x->sb.data_count + 2] = counts >> 16;
+ hx71x->sb.data[hx71x->sb.data_count + 3] = counts >> 24;
+ hx71x->sb.data_count += BYTES_PER_SAMPLE;
+
+ if (hx71x->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(hx71x->sb.data)
+ || force_flush)
+ sensor_bulk_report(&hx71x->sb, oid);
+}
+
+// hx71x ADC query
+static void
+hx71x_read_adc(struct hx71x_adc *hx71x, uint8_t oid)
+{
+ uint32_t start = timer_read_time();
+ // Read from sensor
+ uint_fast8_t gain_channel = hx71x->gain_channel;
+ uint32_t adc = hx71x_raw_read(hx71x->dout, hx71x->sclk, 24 + gain_channel);
+ hx71x->pending_flag = 0;
+ barrier();
+
+ // Extract report from raw data
+ uint32_t counts = adc >> gain_channel;
+ if (counts & 0x800000)
+ counts |= 0xFF000000;
+
+ // Check for errors
+ uint_fast8_t extras_mask = (1 << gain_channel) - 1;
+ if ((adc & extras_mask) != extras_mask) {
+ // Transfer did not complete correctly
+ hx71x->last_error = SAMPLE_ERROR_DESYNC;
+ } else if ((timer_read_time() - start) > (hx71x->rest_ticks * 8)) {
+ // Transfer took too long
+ hx71x->last_error = SAMPLE_ERROR_READ_TOO_LONG;
+ }
+
+ // forever send errors until reset
+ if (hx71x->last_error != 0) {
+ counts = hx71x->last_error;
+ }
+
+ // Add measurement to buffer
+ add_sample(hx71x, oid, counts, false);
+}
+
+// Create a hx71x sensor
+void
+command_config_hx71x(uint32_t *args)
+{
+ struct hx71x_adc *hx71x = oid_alloc(args[0]
+ , command_config_hx71x, sizeof(*hx71x));
+ hx71x->timer.func = hx71x_event;
+ hx71x->pending_flag = 0;
+ uint8_t gain_channel = args[1];
+ if (gain_channel < 1 || gain_channel > 4) {
+ shutdown("HX71x gain/channel out of range 1-4");
+ }
+ hx71x->gain_channel = gain_channel;
+ hx71x->dout = gpio_in_setup(args[2], 1);
+ hx71x->sclk = gpio_out_setup(args[3], 0);
+ gpio_out_write(hx71x->sclk, 1); // put chip in power down state
+}
+DECL_COMMAND(command_config_hx71x, "config_hx71x oid=%c gain_channel=%c"
+ " dout_pin=%u sclk_pin=%u");
+
+// start/stop capturing ADC data
+void
+command_query_hx71x(uint32_t *args)
+{
+ uint8_t oid = args[0];
+ struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x);
+ sched_del_timer(&hx71x->timer);
+ hx71x->pending_flag = 0;
+ hx71x->last_error = 0;
+ hx71x->rest_ticks = args[1];
+ if (!hx71x->rest_ticks) {
+ // End measurements
+ gpio_out_write(hx71x->sclk, 1); // put chip in power down state
+ return;
+ }
+ // Start new measurements
+ gpio_out_write(hx71x->sclk, 0); // wake chip from power down
+ sensor_bulk_reset(&hx71x->sb);
+ irq_disable();
+ hx71x->timer.waketime = timer_read_time() + hx71x->rest_ticks;
+ sched_add_timer(&hx71x->timer);
+ irq_enable();
+}
+DECL_COMMAND(command_query_hx71x, "query_hx71x oid=%c rest_ticks=%u");
+
+void
+command_query_hx71x_status(const uint32_t *args)
+{
+ uint8_t oid = args[0];
+ struct hx71x_adc *hx71x = oid_lookup(oid, command_config_hx71x);
+ irq_disable();
+ const uint32_t start_t = timer_read_time();
+ uint8_t is_data_ready = hx71x_is_data_ready(hx71x);
+ irq_enable();
+ uint8_t pending_bytes = is_data_ready ? BYTES_PER_SAMPLE : 0;
+ sensor_bulk_status(&hx71x->sb, oid, start_t, 0, pending_bytes);
+}
+DECL_COMMAND(command_query_hx71x_status, "query_hx71x_status oid=%c");
+
+// Background task that performs measurements
+void
+hx71x_capture_task(void)
+{
+ if (!sched_check_wake(&wake_hx71x))
+ return;
+ uint8_t oid;
+ struct hx71x_adc *hx71x;
+ foreach_oid(oid, hx71x, command_config_hx71x) {
+ if (hx71x->pending_flag)
+ hx71x_read_adc(hx71x, oid);
+ }
+}
+DECL_TASK(hx71x_capture_task);
diff --git a/test/configs/ar100.config b/test/configs/ar100.config
index 6c917482..61734ab9 100644
--- a/test/configs/ar100.config
+++ b/test/configs/ar100.config
@@ -4,3 +4,4 @@ CONFIG_WANT_DISPLAYS=n
CONFIG_WANT_SOFTWARE_I2C=n
CONFIG_WANT_SOFTWARE_SPI=n
CONFIG_WANT_LIS2DW=n
+CONFIG_WANT_HX71X=n
diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config
index 12cc0922..3e0b2a55 100644
--- a/test/configs/stm32f042.config
+++ b/test/configs/stm32f042.config
@@ -4,3 +4,4 @@ CONFIG_MACH_STM32F042=y
CONFIG_WANT_SOFTWARE_I2C=n
CONFIG_WANT_LIS2DW=n
CONFIG_WANT_LDC1612=n
+CONFIG_WANT_HX71X=n