diff --git a/docs/Load_Cell.md b/docs/Load_Cell.md index 2354eda943..7a32cee03e 100644 --- a/docs/Load_Cell.md +++ b/docs/Load_Cell.md @@ -25,6 +25,7 @@ reference_tare_counts: 12345 * [`hx711`](Config_Reference.md#hx711) * [`hx717`](Config_Reference.md#hx717) * [`ads1220`](Config_Reference.md#ads1220) + * [`cs1237`](#cs1237-notes) - `counts_per_gram: 245`\ _Default Value: None_\ @@ -403,3 +404,41 @@ These sensors are popular but have limitations: - Cannot communicate reset events to the MCU, hiding electrical faults - HX717 (320 Hz) strongly preferred over HX711 (80 Hz) for probing; limit HX711 probing speed to 2 mm/s - HX711 Sample rate is hardware-configured, not software-configurable; 10 SPS versions must be rewired for 80 SPS + +### CS1237 Notes + +The CS1237 is a 24-bit sigma-delta ADC with an integrated PGA, designed as a low-cost alternative to the HX711/HX717 family. It is a drop-in replacement in config — swap `sensor_type: hx711` for `sensor_type: cs1237` and rename `dout_pin` to `drdy_pin`. All calibration, tap validation, and probe logic is identical. + +**Key differences vs HX711/HX717:** +- The data pin is named `drdy_pin` (data-ready, active-low), not `dout_pin`. It doubles as the serial data line per the CS1237 protocol. +- Sample rates are `10 | 40 | 640 | 1280` SPS, defaulting to `1280` for normal probing speeds. + +**Switching from HX711 to CS1237:** + +```ini +# HX711 example: +[load_cell] +sensor_type: hx711 +dout_pin: PA1 +sclk_pin: PA2 +sample_rate: 80 +gain: A-128 + +# CS1237 example: +[load_cell] +sensor_type: cs1237 +drdy_pin: PA1 +sclk_pin: PA2 +sample_rate: 1280 +gain: 128 +ref_output_enable: False +``` + +**Rate recommendation:** +The default `1280` SPS is appropriate for normal probing and matches the stock Elegoo Centauri Carbon 2 firmware and the [Qidi Q2 Mainline Kalico driver](https://github.com/MisterSheikh/Qidi_Q2_Mainline_Klipper/blob/d16a934f5a46395000b99100a15b220df01abf57/klipper_patch/klippy/extras/cs1237.py#L50-L53). `640` SPS can also be appropriate for probing when a lower rate is preferred. For scale use, set `sample_rate: 40`; that rate is too slow for normal probing speeds and can cause excessive overshoot force. The CS1237's 1280 SPS maximum exceeds HX717's 320 SPS maximum. + +**Firmware and wiring:** +Enable **Support CS1237 ADC chip** in the MCU's `make menuconfig` before +building and flashing firmware. The DRDY/DOUT pin is open-drain; the CS1237 +firmware enables its required internal pull-up, so configure the pin normally +(for example, `drdy_pin: PA1`, without `^`). diff --git a/klippy/extras/load_cell/__init__.py b/klippy/extras/load_cell/__init__.py index 2a2dbda90c..b06aec1199 100644 --- a/klippy/extras/load_cell/__init__.py +++ b/klippy/extras/load_cell/__init__.py @@ -6,14 +6,15 @@ from klippy.configfile import ConfigWrapper from klippy.printer import Printer, SubsystemComponentCollection -from . import ads1220, fusion, hx71x +from . import ads1220, cs1237, fusion, hx71x from .load_cell import LoadCell # register sensors that implement BulkAdcSensor def register_components(subsystem: SubsystemComponentCollection): sensors = (hx71x.HX71X_SENSOR_TYPES | ads1220.ADS1220_SENSOR_TYPE - | fusion.LOAD_CELL_FUSION_SENSOR_TYPE) + | fusion.LOAD_CELL_FUSION_SENSOR_TYPE + | cs1237.CS1237_SENSOR_TYPES) for name, sensor in sensors.items(): subsystem.register_component("load_cell_sensors", name, sensor) diff --git a/klippy/extras/load_cell/cs1237.py b/klippy/extras/load_cell/cs1237.py new file mode 100644 index 0000000000..dfdd58e0b0 --- /dev/null +++ b/klippy/extras/load_cell/cs1237.py @@ -0,0 +1,246 @@ +# CS1237 ADC load cell sensor support +# +# Copyright (C) 2026 OpenCentauri Contributors +# Modelled on hx71x.py by Gareth Farrington and +# rewritten for Kalico's tap interface by James Turton . +# Drop-in alternative to hx71x for use with [load_cell] tap interface. +# +# Usage (printer.cfg): +# +# [load_cell] +# sensor_type: cs1237 +# drdy_pin: # DRDY / data pin +# sclk_pin: # SCLK pin +# sample_rate: 1280 # 10 | 40 | 640 | 1280 (default 1280) +# gain: 128 # 1 | 2 | 64 | 128 (default 128) +# ref_output_enable: False # leave CC2 REFOUT disabled (default) +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +from klippy.mcu import MCU + +from .. import bulk_sensor +from .interfaces import BulkAdcData, BulkAdcDataCallback, LoadCellSensor + +# ---- Constants ---- + +UPDATE_INTERVAL = 0.10 +SAMPLE_ERROR_DESYNC = -0x80000000 +SAMPLE_ERROR_LONG_READ = 0x40000000 +DEFAULT_SAMPLE_RATE = 1280 +DEFAULT_GAIN = 128 +DEFAULT_REF_OUTPUT_ENABLE = False +CS1237_CONFIG_READ = 0x56 +CS1237_CONFIG_WRITE = 0x65 +CS1237_CONFIG_RETRIES = 10 + +# CS1237 speed register encodings -> SPS +_SPEED_MAP = {10: 0, 40: 1, 640: 2, 1280: 3} + +# CS1237 PGA gain register encodings +_GAIN_MAP = {1: 0, 2: 1, 64: 2, 128: 3} + + +def _make_config_byte(speed_reg, gain_reg, ref_output_enable): + return ((speed_reg & 0x3) << 4) | ((gain_reg & 0x3) << 2) | ( + 0 if ref_output_enable else 0x40 + ) + + +class CS1237(LoadCellSensor): + """CS1237 24-bit ADC load cell sensor driver. + + Implements the same LoadCellSensor interface as HX71x so it can be + used as a drop-in replacement with [load_cell] / load_cell_probe. + """ + + 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 + + # ---- Pin setup ---- + drdy_pin_name = config.get("drdy_pin") + sclk_pin_name = config.get("sclk_pin") + ppins = printer.lookup_object("pins") + drdy_ppin = ppins.lookup_pin(drdy_pin_name) + sclk_ppin = ppins.lookup_pin(sclk_pin_name) + mcu: MCU = drdy_ppin["chip"] + self.mcu: MCU = mcu + self.oid = mcu.create_oid() + if sclk_ppin["chip"] is not mcu: + raise config.error( + "%s: drdy_pin and sclk_pin must be on the same MCU" + % self.name + ) + self.drdy_pin = drdy_ppin["pin"] + self.sclk_pin = sclk_ppin["pin"] + + # ---- Sample rate ---- + self.sps = config.getchoice( + "sample_rate", + {str(k): k for k in _SPEED_MAP}, + default=str(DEFAULT_SAMPLE_RATE), + ) + self.speed_reg = _SPEED_MAP[self.sps] + + # ---- Gain ---- + self.gain_val = config.getchoice( + "gain", + {str(k): k for k in _GAIN_MAP}, + default=str(DEFAULT_GAIN), + ) + self.gain_reg = _GAIN_MAP[self.gain_val] + self.ref_output_enable = config.getboolean( + "ref_output_enable", default=DEFAULT_REF_OUTPUT_ENABLE + ) + self.config_byte = _make_config_byte( + self.speed_reg, self.gain_reg, self.ref_output_enable + ) + + # ---- Bulk sensor setup ---- + chip_smooth = self.sps * UPDATE_INTERVAL * 2 + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, " MCU: + return self.mcu + + def get_samples_per_second(self) -> int: + return self.sps + + def get_range(self) -> tuple: + """CS1237 output is 24-bit two's complement.""" + return -0x800000, 0x7FFFFF + + def get_channel_count(self) -> int: + return 1 + + def add_client(self, callback: BulkAdcDataCallback): + self.batch_bulk.add_client(callback) + + def attach_load_cell_probe(self, load_cell_probe_oid: int): + self.attach_probe_cmd.send([self.oid, load_cell_probe_oid]) + + # ---- Internal ---- + + def _build_config(self): + self.config_cs1237_cmd = self.mcu.lookup_query_command( + "cs1237_config oid=%c command=%c value=%c", + "cs1237_config_response oid=%c success=%c value=%c", + oid=self.oid, + cq=self.mcu.alloc_command_queue(), + ) + self.query_cs1237_cmd = self.mcu.lookup_command( + "query_cs1237 oid=%c rest_ticks=%u" + ) + self.attach_probe_cmd = self.mcu.lookup_command( + "cs1237_attach_load_cell_probe oid=%c load_cell_probe_oid=%c" + ) + self.ffreader.setup_query_command( + "query_cs1237_status oid=%c", + oid=self.oid, + cq=self.mcu.alloc_command_queue(), + ) + + def _convert_samples(self, samples): + adc_factor = 1.0 / (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 + samples[count] = (round(ptime, 6), val, round(val * adc_factor, 9)) + count += 1 + del samples[count:] + + def _configure_sensor(self): + assert self.config_cs1237_cmd is not None + for _ in range(CS1237_CONFIG_RETRIES): + write = self.config_cs1237_cmd.send( + [self.oid, CS1237_CONFIG_WRITE, self.config_byte] + ) + if not write["success"]: + continue + read = self.config_cs1237_cmd.send( + [self.oid, CS1237_CONFIG_READ, 0] + ) + if read["success"] and read["value"] == self.config_byte: + return + raise self.printer.command_error( + "%s: CS1237 configuration readback failed" % self.name + ) + + def _start_measurements(self): + self._configure_sensor() + self.consecutive_fails = 0 + self.last_error_count = 0 + rest_ticks = self.mcu.seconds_to_clock(1.0 / (10.0 * self.sps)) + self.query_cs1237_cmd.send([self.oid, rest_ticks]) + logging.info("cs1237 starting '%s' measurements", self.name) + self.ffreader.note_start() + + def _finish_measurements(self): + if self.printer.is_shutdown(): + return + self.query_cs1237_cmd.send_wait_ack([self.oid, 0]) + self.ffreader.note_end() + logging.info("cs1237 finished '%s' measurements", self.name) + + def _process_batch(self, eventtime) -> BulkAdcData: + 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": errors, + "overflows": overflows, + } + + +CS1237_SENSOR_TYPES = {"cs1237": CS1237} diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index dc6fd1bffa..20babc8cc2 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -3,7 +3,7 @@ # Copyright (C) 2025 Gareth Farrington # # This file may be distributed under the terms of the GNU GPLv3 license. -from klippy.extras.load_cell import ads1220, fusion, hx71x +from klippy.extras.load_cell import ads1220, cs1237, fusion, hx71x from klippy.extras.load_cell.load_cell_probe import LoadCellPrinterProbe from klippy.printer import Printer, SubsystemComponentCollection @@ -11,7 +11,8 @@ # register sensors that implement LoadCellSensor def register_components(subsystem: SubsystemComponentCollection): sensors = (hx71x.HX71X_SENSOR_TYPES | ads1220.ADS1220_SENSOR_TYPE - | fusion.LOAD_CELL_FUSION_SENSOR_TYPE) + | fusion.LOAD_CELL_FUSION_SENSOR_TYPE + | cs1237.CS1237_SENSOR_TYPES) for name, sensor in sensors.items(): subsystem.register_component("load_cell_probe_sensors", name, sensor) diff --git a/src/Kconfig b/src/Kconfig index d679cef9a4..a8bc7165fa 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -177,6 +177,10 @@ config WANT_HX71X bool depends on HAVE_GPIO default y +config WANT_CS1237 + bool + depends on HAVE_GPIO + default n config WANT_HX711S bool depends on HAVE_GPIO @@ -208,12 +212,12 @@ config WANT_GPIO_I2C config NEED_SENSOR_BULK bool depends on WANT_ADXL345 || WANT_LIS2DW || WANT_MPU9250 || WANT_ICM20948 \ - || WANT_HX71X || WANT_ADS1220 \ + || WANT_HX71X || WANT_CS1237 || WANT_ADS1220 \ || WANT_LDC1612 || WANT_SENSOR_ANGLE default y config NEED_LOAD_CELL_PROBE bool - depends on WANT_HX71X || WANT_ADS1220 + depends on WANT_HX71X || WANT_CS1237 || WANT_ADS1220 default y config NEED_LOAD_CELL_FUSION bool @@ -292,6 +296,10 @@ config WANT_THERMOCOUPLE config WANT_HX71X bool "Support HX711 and HX717 ADC chips" depends on HAVE_GPIO +config WANT_CS1237 + bool "Support CS1237 ADC chip" + depends on HAVE_GPIO + default n config WANT_HX711S bool "Support HX711S strain gauge probe sensors" depends on HAVE_GPIO diff --git a/src/Makefile b/src/Makefile index 3e7754af4e..b8d0db3eee 100644 --- a/src/Makefile +++ b/src/Makefile @@ -24,6 +24,7 @@ src-$(CONFIG_WANT_LDC1612) += sensor_ldc1612.c src-$(CONFIG_WANT_MPU9250) += sensor_mpu9250.c src-$(CONFIG_WANT_ICM20948) += sensor_icm20948.c src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c +src-$(CONFIG_WANT_CS1237) += sensor_cs1237.c src-$(CONFIG_WANT_ADS1220) += sensor_ads1220.c src-$(CONFIG_WANT_SENSOR_ANGLE) += sensor_angle.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c diff --git a/src/sensor_cs1237.c b/src/sensor_cs1237.c new file mode 100644 index 0000000000..d8ae159466 --- /dev/null +++ b/src/sensor_cs1237.c @@ -0,0 +1,308 @@ +// CS1237 ADC sensor support. +// +// The CS1237 uses a half-duplex data-ready/data pin. Keep the Klipper +// command and bulk-reporting interface separate from the wire protocol so +// this remains compatible with the Kalico load-cell probe code. + +#include + +#include "board/gpio.h" +#include "board/irq.h" +#include "board/misc.h" +#include "basecmd.h" +#include "command.h" +#include "load_cell_probe.h" +#include "sched.h" +#include "sensor_bulk.h" + +#define CS1237_BITS 24 +#define CS1237_SAMPLE_BYTES 4 +#define CS1237_CONFIG_READ 0x56 +#define CS1237_CONFIG_WRITE 0x65 +#define CS1237_CONFIG_TIMEOUT_US 350000 + +#define SAMPLE_ERROR_DESYNC ((uint32_t)0x80000000) +#define SAMPLE_ERROR_READ_TOO_LONG ((uint32_t)0x40000000) + +struct cs1237_sensor { + struct timer timer; + struct gpio_in drdy_pin; + struct gpio_out dio_pin; + struct gpio_out sclk_pin; + uint32_t rest_ticks; + uint32_t last_error; + uint8_t flags; + struct sensor_bulk sb; + struct load_cell_probe *lcp; +}; + +enum { + FLAG_PENDING = 1 << 0, + FLAG_OVERFLOW = 1 << 1, +}; + +static struct task_wake cs1237_wake; + +void command_config_cs1237(uint32_t *args); + +static void +cs1237_delay(void) +{ + uint32_t end = timer_read_time() + timer_from_us(1); + while (timer_is_before(timer_read_time(), end)) + ; +} + +static void +cs1237_clock(struct cs1237_sensor *cs) +{ + irq_disable(); + gpio_out_toggle_noirq(cs->sclk_pin); + cs1237_delay(); + gpio_out_toggle_noirq(cs->sclk_pin); + irq_enable(); + cs1237_delay(); +} + +static void +cs1237_clock_count(struct cs1237_sensor *cs, uint8_t count) +{ + while (count--) + cs1237_clock(cs); +} + +static uint8_t +cs1237_clock_read(struct cs1237_sensor *cs) +{ + irq_disable(); + gpio_out_toggle_noirq(cs->sclk_pin); + cs1237_delay(); + uint8_t bit = gpio_in_read(cs->drdy_pin); + gpio_out_toggle_noirq(cs->sclk_pin); + irq_enable(); + cs1237_delay(); + return bit; +} + +static void +cs1237_clock_write(struct cs1237_sensor *cs, uint8_t bit) +{ + gpio_out_write(cs->dio_pin, bit); + cs1237_clock(cs); +} + +static int32_t +cs1237_read_sample(struct cs1237_sensor *cs) +{ + uint32_t raw = 0; + for (int i = 0; i < CS1237_BITS; i++) + raw = (raw << 1) | cs1237_clock_read(cs); + + // Clock 25 is configuration-write status; clock 26 is reserved low; + // clock 27 must release DRDY/DOUT high before the next conversion. + (void)cs1237_clock_read(cs); + uint8_t reserved = cs1237_clock_read(cs); + uint8_t released = cs1237_clock_read(cs); + if (reserved || !released) + return (int32_t)SAMPLE_ERROR_DESYNC; + if (raw & 0x800000) + raw |= 0xff000000; + return (int32_t)raw; +} + +static uint_fast8_t +cs1237_is_data_ready(struct cs1237_sensor *cs) +{ + return !gpio_in_read(cs->drdy_pin); +} + +static uint_fast8_t +cs1237_wait_ready(struct cs1237_sensor *cs) +{ + uint32_t timeout = timer_read_time() + + timer_from_us(CS1237_CONFIG_TIMEOUT_US); + while (!cs1237_is_data_ready(cs)) { + if (timer_is_before(timeout, timer_read_time())) + return 0; + } + return 1; +} + +static uint_fast8_t +cs1237_config_transfer(struct cs1237_sensor *cs, uint8_t command, + uint8_t value, uint8_t *readback) +{ + if (!cs1237_wait_ready(cs)) + return 0; + + // Flush the current conversion and enter the 46-clock config window. + cs1237_clock_count(cs, 27); + + // DIO is input for DRDY/data and output for this command/register write. + gpio_out_reset(cs->dio_pin, 1); + cs1237_clock_count(cs, 2); + for (int i = 6; i >= 0; i--) + cs1237_clock_write(cs, (command >> i) & 1); + // Clock 37 switches DIO from the command phase to register data. + if (command == CS1237_CONFIG_READ) + gpio_in_reset(cs->drdy_pin, 1); + cs1237_clock(cs); + + uint8_t result = 0; + for (int i = 7; i >= 0; i--) { + if (command == CS1237_CONFIG_READ) + result = (result << 1) | cs1237_clock_read(cs); + else + cs1237_clock_write(cs, (value >> i) & 1); + } + + // Return the shared DIO pin to input mode before the next conversion. + if (command != CS1237_CONFIG_READ) + gpio_in_reset(cs->drdy_pin, 1); + cs1237_clock(cs); + uint8_t released = gpio_in_read(cs->drdy_pin); + gpio_out_write(cs->sclk_pin, 0); + if (!released) + return 0; + *readback = result; + return 1; +} + +static uint_fast8_t +cs1237_event(struct timer *timer) +{ + struct cs1237_sensor *cs = container_of(timer, struct cs1237_sensor, + timer); + uint32_t rest_ticks = cs->rest_ticks; + if (cs->flags & FLAG_PENDING) { + cs->flags = FLAG_PENDING | FLAG_OVERFLOW; + cs->sb.possible_overflows++; + rest_ticks *= 4; + } else if (cs1237_is_data_ready(cs)) { + cs->flags = FLAG_PENDING; + sched_wake_task(&cs1237_wake); + rest_ticks *= 8; + } + cs->timer.waketime += rest_ticks; + return SF_RESCHEDULE; +} + +static void +cs1237_add_sample(struct cs1237_sensor *cs, uint8_t oid, int32_t sample) +{ + if (cs->sb.data_count + CS1237_SAMPLE_BYTES > ARRAY_SIZE(cs->sb.data)) + sensor_bulk_report(&cs->sb, oid); + cs->sb.data[cs->sb.data_count++] = sample; + cs->sb.data[cs->sb.data_count++] = sample >> 8; + cs->sb.data[cs->sb.data_count++] = sample >> 16; + cs->sb.data[cs->sb.data_count++] = sample >> 24; +} + +static void +cs1237_capture(struct cs1237_sensor *cs, uint8_t oid) +{ + int32_t sample; + uint8_t flags; + if (!cs1237_is_data_ready(cs)) + cs->last_error = SAMPLE_ERROR_DESYNC; + sample = cs->last_error ? (int32_t)cs->last_error + : cs1237_read_sample(cs); + if (sample == (int32_t)SAMPLE_ERROR_DESYNC) + cs->last_error = SAMPLE_ERROR_DESYNC; + + irq_disable(); + flags = cs->flags; + cs->flags = 0; + irq_enable(); + if (flags & FLAG_OVERFLOW) + cs->last_error = SAMPLE_ERROR_READ_TOO_LONG; + if (cs->last_error) + sample = (int32_t)cs->last_error; + else if (cs->lcp) + load_cell_probe_report_sample(cs->lcp, sample); + cs1237_add_sample(cs, oid, sample); +} + +void +cs1237_capture_task(void) +{ + if (!sched_check_wake(&cs1237_wake)) + return; + uint8_t oid; + struct cs1237_sensor *cs; + foreach_oid(oid, cs, command_config_cs1237) { + if (cs->flags & FLAG_PENDING) + cs1237_capture(cs, oid); + } +} +DECL_TASK(cs1237_capture_task); + +void +command_config_cs1237(uint32_t *args) +{ + struct cs1237_sensor *cs = oid_alloc( + args[0], command_config_cs1237, sizeof(*cs)); + cs->dio_pin = gpio_out_setup(args[1], 1); + cs->drdy_pin = gpio_in_setup(args[1], 1); + cs->sclk_pin = gpio_out_setup(args[2], 0); +} +DECL_COMMAND(command_config_cs1237, + "config_cs1237 oid=%c drdy_pin=%u sclk_pin=%u"); + +void +command_cs1237_config(uint32_t *args) +{ + struct cs1237_sensor *cs = oid_lookup(args[0], command_config_cs1237); + uint8_t command = args[1]; + uint8_t value = args[2]; + uint8_t readback = 0; + uint8_t success = 0; + if ((command == CS1237_CONFIG_READ) + || (command == CS1237_CONFIG_WRITE && !(value & 0x83))) + success = cs1237_config_transfer(cs, command, value, &readback); + sendf("cs1237_config_response oid=%c success=%c value=%c", + args[0], success, readback); +} +DECL_COMMAND(command_cs1237_config, + "cs1237_config oid=%c command=%c value=%c"); + +void +command_query_cs1237(uint32_t *args) +{ + struct cs1237_sensor *cs = oid_lookup(args[0], command_config_cs1237); + sched_del_timer(&cs->timer); + cs->flags = 0; + cs->last_error = 0; + cs->rest_ticks = args[1]; + if (!cs->rest_ticks) { + sensor_bulk_reset(&cs->sb); + return; + } + sensor_bulk_reset(&cs->sb); + cs->timer.waketime = timer_read_time() + cs->rest_ticks; + cs->timer.func = cs1237_event; + sched_add_timer(&cs->timer); +} +DECL_COMMAND(command_query_cs1237, "query_cs1237 oid=%c rest_ticks=%u"); + +void +command_query_cs1237_status(uint32_t *args) +{ + struct cs1237_sensor *cs = oid_lookup(args[0], command_config_cs1237); + irq_disable(); + uint8_t pending_bytes = cs1237_is_data_ready(cs) ? CS1237_SAMPLE_BYTES : 0; + uint32_t clock = timer_read_time(); + struct sensor_bulk sb = cs->sb; + irq_enable(); + sensor_bulk_status(&sb, args[0], clock, 0, pending_bytes); +} +DECL_COMMAND(command_query_cs1237_status, "query_cs1237_status oid=%c"); + +void +command_cs1237_attach_load_cell_probe(uint32_t *args) +{ + struct cs1237_sensor *cs = oid_lookup(args[0], command_config_cs1237); + cs->lcp = load_cell_probe_oid_lookup(args[1]); +} +DECL_COMMAND(command_cs1237_attach_load_cell_probe, + "cs1237_attach_load_cell_probe oid=%c load_cell_probe_oid=%c"); diff --git a/test/configs/stm32f405.config b/test/configs/stm32f405.config index bd99c762d9..f341d0ab33 100644 --- a/test/configs/stm32f405.config +++ b/test/configs/stm32f405.config @@ -1,3 +1,4 @@ # Base config file for STM32F405 ARM processor CONFIG_MACH_STM32=y CONFIG_MACH_STM32F405=y +CONFIG_WANT_CS1237=y diff --git a/test/klippy/cs1237.cfg b/test/klippy/cs1237.cfg new file mode 100644 index 0000000000..ffe447d01a --- /dev/null +++ b/test/klippy/cs1237.cfg @@ -0,0 +1,11 @@ +# Test config for CS1237 +[mcu] +serial: /dev/ttyACM0 +[printer] +kinematics: none +max_velocity: 300 +max_accel: 3000 +[load_cell my_cs1237] +sensor_type: cs1237 +drdy_pin: PA6 +sclk_pin: PA7 diff --git a/test/klippy/cs1237.test b/test/klippy/cs1237.test new file mode 100644 index 0000000000..6c990c43ea --- /dev/null +++ b/test/klippy/cs1237.test @@ -0,0 +1,4 @@ +# Tests for CS1237 load-cell support +DICTIONARY stm32f405.dict +CONFIG cs1237.cfg +G4 P1000 diff --git a/test/test_cs1237.py b/test/test_cs1237.py new file mode 100644 index 0000000000..3c3e95ef58 --- /dev/null +++ b/test/test_cs1237.py @@ -0,0 +1,108 @@ +from klippy.extras.load_cell.cs1237 import ( + CS1237, + CS1237_CONFIG_READ, + CS1237_CONFIG_WRITE, + DEFAULT_GAIN, + DEFAULT_REF_OUTPUT_ENABLE, + DEFAULT_SAMPLE_RATE, + SAMPLE_ERROR_DESYNC as CS1237_ERROR, + _GAIN_MAP, + _SPEED_MAP, + _make_config_byte, +) + + +class _Reader: + def __init__(self, error): + self.error = error + + def get_last_overflows(self): + return 0 + + def pull_samples(self): + return [(0.0, self.error)] + + +class _OverflowReader: + def __init__(self): + self.overflows = 0 + + def get_last_overflows(self): + return self.overflows + + def pull_samples(self): + self.overflows = 1 + return [] + + +class _ConfigCommand: + def __init__(self, responses): + self.responses = iter(responses) + self.calls = [] + + def send(self, data): + self.calls.append(data) + return next(self.responses) + + +def test_error_batch_survives_sensor_restart(): + sensor = CS1237.__new__(CS1237) + setattr(sensor, "ffreader", _Reader(CS1237_ERROR)) + sensor.last_error_count = 0 + sensor.consecutive_fails = 0 + sensor.name = "test" + sensor._finish_measurements = lambda: None + sensor._start_measurements = lambda: setattr(sensor, "last_error_count", 0) + + result = sensor._process_batch(0.0) + + assert result["errors"] == 1 + assert result["overflows"] == 0 + + +def test_overflow_batch_survives_sensor_restart(): + sensor = CS1237.__new__(CS1237) + reader = _OverflowReader() + setattr(sensor, "ffreader", reader) + sensor.last_error_count = 0 + sensor.consecutive_fails = 4 + sensor.name = "test" + sensor._finish_measurements = lambda: None + sensor._start_measurements = lambda: setattr(reader, "overflows", 0) + + result = sensor._process_batch(0.0) + + assert result["errors"] == 0 + assert result["overflows"] == 1 + + +def test_configure_sensor_retries_until_readback_matches(): + sensor = CS1237.__new__(CS1237) + sensor.oid = 7 + sensor.config_byte = 0x7C + command = _ConfigCommand( + ( + {"success": 0, "value": 0}, + {"success": 1, "value": 0}, + {"success": 1, "value": 0x7C}, + ) + ) + setattr(sensor, "config_cs1237_cmd", command) + + sensor._configure_sensor() + + assert command.calls == [ + [7, CS1237_CONFIG_WRITE, 0x7C], + [7, CS1237_CONFIG_WRITE, 0x7C], + [7, CS1237_CONFIG_READ, 0], + ] + + +def test_config_byte_disables_refout_for_default_and_scale_rates(): + assert _make_config_byte( + _SPEED_MAP[DEFAULT_SAMPLE_RATE], + _GAIN_MAP[DEFAULT_GAIN], + DEFAULT_REF_OUTPUT_ENABLE, + ) == 0x7C + assert _make_config_byte(_SPEED_MAP[40], 3, False) == 0x5C + assert _make_config_byte(3, 3, True) == 0x3C