diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 5815b760d9..272955f21c 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -5953,6 +5953,31 @@ sensor_type: # decreasing force value when placed under load. ``` +#### CS1237 + +The CS1237 sensor backend supports both `[load_cell]` and +`[load_cell_probe]`. + +``` +[load_cell my_cs1237] +sensor_type: cs1237 +sclk_pin: +# The pin connected to the CS1237 clock line. This parameter must be +# provided. +dout_pin: +# The pin connected to the bidirectional CS1237 DOUT line. This parameter +# must be provided. +#sample_rate: 640 +# Valid values for sample_rate are 10, 40, 640, or 1280. The default is 640. +#gain: 128 +# Valid values for gain are 1, 2, 64, or 128. The default is 128. +#reference_output: True +# Enable the CS1237 REFO reference output. The default is True. +``` + +Both pins must be on the same MCU. DOUT is bidirectional during +configuration. + #### HX711 This is a 24 bit low sample rate chip using "bit-bang" communications. It is suitable for filament scales. diff --git a/docs/Load_Cell.md b/docs/Load_Cell.md index a5d6261d94..bbeea1cb82 100644 --- a/docs/Load_Cell.md +++ b/docs/Load_Cell.md @@ -27,6 +27,7 @@ reference_tare_counts: 12345 * [`ads1220`](Config_Reference.md#ads1220) * [`ads131m02`](Config_Reference.md#ads131m02) * [`ads131m04`](Config_Reference.md#ads131m04) + * [`cs1237`](Config_Reference.md#cs1237) - `counts_per_gram: 245`\ _Default Value: None_\ diff --git a/klippy/extras/load_cell/__init__.py b/klippy/extras/load_cell/__init__.py index ecaeff859e..dfe33727c0 100644 --- a/klippy/extras/load_cell/__init__.py +++ b/klippy/extras/load_cell/__init__.py @@ -6,7 +6,7 @@ from klippy.configfile import ConfigWrapper from klippy.printer import Printer, SubsystemComponentCollection -from . import ads131m0x, ads1220, hx71x +from . import ads131m0x, ads1220, cs1237, hx71x from .load_cell import LoadCell @@ -16,6 +16,7 @@ def register_components(subsystem: SubsystemComponentCollection): hx71x.HX71X_SENSOR_TYPES | ads1220.ADS1220_SENSOR_TYPE | ads131m0x.ADS131M0X_SENSOR_TYPES + | cs1237.CS1237_SENSOR_TYPE ) 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..0f2fccaa3d --- /dev/null +++ b/klippy/extras/load_cell/cs1237.py @@ -0,0 +1,152 @@ +# Chipsea CS1237 support +# +# Copyright (C) 2026 Xiaoyue Cui <2508041672@qq.com> +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +from .. import bulk_sensor + +UPDATE_INTERVAL = 0.10 + + +class CS1237: + def __init__(self, config): + self.printer = printer = config.get_printer() + self.name = config.get_name().split()[-1] + self.last_error_count = 0 + self._sensor_errors = {} + + rate_options = {"10": 0, "40": 1, "640": 2, "1280": 3} + gain_options = {"1": 0, "2": 1, "64": 2, "128": 3} + self.sps = config.getchoice( + "sample_rate", + {key: int(key) for key in rate_options}, + default="640", + ) + rate = config.getchoice("sample_rate", rate_options, default="640") + gain = config.getchoice("gain", gain_options, default="128") + self.reference_output = config.getboolean("reference_output", True) + ref_off = 0 if self.reference_output else 1 + self.chip_config = (ref_off << 6) | (rate << 4) | (gain << 2) + + 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"] + if sclk_ppin["chip"] is not mcu: + raise config.error( + "CS1237 config error: All pins must be " + "connected to the same MCU" + ) + self.dout_pin = dout_ppin["pin"] + self.sclk_pin = sclk_ppin["pin"] + self.oid = mcu.create_oid() + + chip_smooth = self.sps * UPDATE_INTERVAL * 2 + self.ffreader = bulk_sensor.FixedFreqReader(mcu, chip_smooth, "> 24) & 0xFF + if top_byte != 0x00 and top_byte != 0xFF: + self.last_error_count += 1 + continue + samples[count] = ( + round(ptime, 6), + value, + round(value * adc_factor, 9), + ) + count += 1 + del samples[count:] + + def _start_measurements(self): + rest_ticks = self.mcu.seconds_to_clock(1.0 / (10.0 * self.sps)) + self.query_cmd.send([self.oid, rest_ticks]) + logging.info( + "CS1237 starting '%s' measurements at %d SPS", self.name, self.sps + ) + self.ffreader.note_start() + + def _finish_measurements(self): + if self.printer.is_shutdown(): + return + self.query_cmd.send_wait_ack([self.oid, 0]) + self.ffreader.note_end() + logging.info("CS1237 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(), + } + + +CS1237_SENSOR_TYPE = {"cs1237": CS1237} diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index eac29855b1..cc46c58790 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 ads131m0x, ads1220, hx71x +from klippy.extras.load_cell import ads131m0x, ads1220, cs1237, hx71x from klippy.extras.load_cell.load_cell_probe import LoadCellPrinterProbe from klippy.extras.load_cell.tap_analysis import TapClassifierModule from klippy.extras.load_cell.tap_quality_classifier import TapQualityClassifier @@ -16,6 +16,7 @@ def register_components(subsystem: SubsystemComponentCollection): hx71x.HX71X_SENSOR_TYPES | ads1220.ADS1220_SENSOR_TYPE | ads131m0x.ADS131M0X_SENSOR_TYPES + | cs1237.CS1237_SENSOR_TYPE ) for name, sensor in sensors.items(): subsystem.register_component("load_cell_probe_sensors", name, sensor) diff --git a/src/Kconfig b/src/Kconfig index 8e2fb0c5d9..2900d52a5e 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -204,6 +204,10 @@ config WANT_ADS131M0X bool depends on WANT_SPI default y +config WANT_CS1237 + bool + depends on HAVE_GPIO + default y config WANT_LDC1612 bool depends on WANT_I2C @@ -227,12 +231,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_ADS131M0X \ + || WANT_HX71X || WANT_ADS1220 || WANT_ADS131M0X || WANT_CS1237 \ || WANT_LDC1612 || WANT_SENSOR_ANGLE default y config NEED_LOAD_CELL_PROBE bool - depends on WANT_HX71X || WANT_ADS1220 || WANT_ADS131M0X + depends on WANT_HX71X || WANT_ADS1220 || WANT_ADS131M0X || WANT_CS1237 default y config NEED_SOS_FILTER bool @@ -317,6 +321,9 @@ config WANT_ADS1220 config WANT_ADS131M0X bool "Support ADS131M02 and ADS131M04 ADC chips" depends on WANT_SPI +config WANT_CS1237 + bool "Support CS1237 ADC chips" + depends on HAVE_GPIO config WANT_LDC1612 bool "Support ldc1612 eddy current sensor chip" depends on WANT_I2C diff --git a/src/Makefile b/src/Makefile index ecefd62b53..2914a26315 100644 --- a/src/Makefile +++ b/src/Makefile @@ -26,6 +26,7 @@ src-$(CONFIG_WANT_ICM20948) += sensor_icm20948.c src-$(CONFIG_WANT_HX71X) += sensor_hx71x.c src-$(CONFIG_WANT_ADS1220) += sensor_ads1220.c src-$(CONFIG_WANT_ADS131M0X) += sensor_ads131m0x.c +src-$(CONFIG_WANT_CS1237) += sensor_cs1237.c src-$(CONFIG_WANT_SENSOR_ANGLE) += sensor_angle.c src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c src-$(CONFIG_NEED_SOS_FILTER) += sos_filter.c diff --git a/src/sensor_cs1237.c b/src/sensor_cs1237.c new file mode 100644 index 0000000000..c2f7975811 --- /dev/null +++ b/src/sensor_cs1237.c @@ -0,0 +1,313 @@ +// Support for the Chipsea CS1237 24-bit ADC +// +// Copyright (C) 2026 Xiaoyue Cui <2508041672@qq.com> +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include +#include "autoconf.h" // CONFIG_MACH_AVR +#include "basecmd.h" // oid_alloc +#include "board/gpio.h" // gpio_out_write +#include "board/irq.h" // irq_poll +#include "board/misc.h" // timer_read_time +#include "command.h" // DECL_COMMAND +#include "sched.h" // sched_add_timer +#include "sensor_bulk.h" // sensor_bulk_report +#include "load_cell_probe.h" // load_cell_probe_report_sample + +struct cs1237_adc { + struct timer timer; + uint32_t rest_ticks; + uint8_t config; + uint8_t flags; + uint8_t is_configured; + struct gpio_in dout; + struct gpio_out dout_out; + struct gpio_out sclk; + struct sensor_bulk sb; + struct load_cell_probe *lce; +}; + +enum { + CS_PENDING = 1<<0, CS_OVERFLOW = 1<<1, + CS_CONFIG_PENDING = 1<<2, CS_CONFIG_VERIFY = 1<<3, +}; + +// Internal errors transmitted in sample reports +enum { + SE_DESYNC = 1, SE_READ_TOO_LONG, SE_CONFIG +}; + +DECL_ENUMERATION("cs1237_error:", "SERIAL_DATA_DESYNCHRONISED", SE_DESYNC); +DECL_ENUMERATION("cs1237_error:", "READ_TOOK_TOO_LONG", SE_READ_TOO_LONG); +DECL_ENUMERATION("cs1237_error:", "CONFIG_NOT_ACCEPTED", SE_CONFIG); + +#define BYTES_PER_SAMPLE 4 +#define CS1237_WRITE_CONFIG 0x65 + +static struct task_wake wake_cs1237; + +// The data sheet specifies at least 455ns for each SCLK high/low pulse. +#define MIN_PULSE_TIME nsecs_to_ticks(455) + +static uint32_t +nsecs_to_ticks(uint32_t ns) +{ + return DIV_ROUND_UP(timer_from_us(ns * 1000), 1000000); +} + +static void +cs1237_delay_noirq(void) +{ + if (CONFIG_MACH_AVR) { + // Avoid slow timer calculations for this sub-microsecond delay. + asm("nop\n nop\n nop\n nop\n nop\n" + " nop\n nop\n nop\n nop\n nop"); + return; + } + uint32_t end = timer_read_time() + MIN_PULSE_TIME; + while (timer_is_before(timer_read_time(), end)) + ; +} + +static void +cs1237_delay(void) +{ + if (CONFIG_MACH_AVR) + // Pin and interrupt operations provide the remaining low pulse time. + return; + uint32_t end = timer_read_time() + MIN_PULSE_TIME; + while (timer_is_before(timer_read_time(), end)) + irq_poll(); +} + +static uint32_t +cs1237_read_bits(struct cs1237_adc *cs, uint_fast8_t num_bits) +{ + uint32_t value = 0; + while (num_bits--) { + irq_disable(); + gpio_out_write(cs->sclk, 1); + cs1237_delay_noirq(); + uint_fast8_t bit = gpio_in_read(cs->dout); + gpio_out_write(cs->sclk, 0); + irq_enable(); + cs1237_delay(); + value = (value << 1) | bit; + } + return value; +} + +static void +cs1237_clock_pulses(struct cs1237_adc *cs, uint_fast8_t count) +{ + while (count--) { + irq_disable(); + gpio_out_write(cs->sclk, 1); + cs1237_delay_noirq(); + gpio_out_write(cs->sclk, 0); + irq_enable(); + cs1237_delay(); + } +} + +static void +cs1237_write_bits(struct cs1237_adc *cs, uint32_t value, uint32_t mask) +{ + while (mask) { + gpio_out_write(cs->dout_out, !!(value & mask)); + cs1237_delay(); + irq_disable(); + gpio_out_write(cs->sclk, 1); + cs1237_delay_noirq(); + gpio_out_write(cs->sclk, 0); + irq_enable(); + cs1237_delay(); + mask >>= 1; + } +} + +// Called immediately after the 24 ADC data clocks. Clocks 25-29 announce a +// command, clocks 30-36 contain the seven bit command, clock 37 changes bus +// direction, clocks 38-45 contain Config, and clock 46 releases DRDY/DOUT. +static void +cs1237_write_config(struct cs1237_adc *cs) +{ + cs1237_clock_pulses(cs, 5); + gpio_out_reset(cs->dout_out, 0); + cs1237_write_bits(cs, CS1237_WRITE_CONFIG, 0x40); + cs1237_clock_pulses(cs, 1); + cs1237_write_bits(cs, cs->config, 0x80); + gpio_in_reset(cs->dout, 0); + cs1237_clock_pulses(cs, 1); +} + +static uint_fast8_t +cs1237_is_data_ready(struct cs1237_adc *cs) +{ + return !gpio_in_read(cs->dout); +} + +static uint_fast8_t +cs1237_event(struct timer *timer) +{ + struct cs1237_adc *cs = container_of(timer, struct cs1237_adc, timer); + uint32_t rest_ticks = cs->rest_ticks; + if (cs->flags & CS_PENDING) { + cs->sb.possible_overflows++; + cs->flags |= CS_OVERFLOW; + rest_ticks *= 4; + } else if (cs1237_is_data_ready(cs)) { + cs->flags |= CS_PENDING; + sched_wake_task(&wake_cs1237); + rest_ticks *= 8; + } + cs->timer.waketime += rest_ticks; + return SF_RESCHEDULE; +} + +static void +add_sample(struct cs1237_adc *cs, uint8_t oid, uint32_t counts) +{ + cs->sb.data[cs->sb.data_count] = counts; + cs->sb.data[cs->sb.data_count + 1] = counts >> 8; + cs->sb.data[cs->sb.data_count + 2] = counts >> 16; + cs->sb.data[cs->sb.data_count + 3] = counts >> 24; + cs->sb.data_count += BYTES_PER_SAMPLE; + if (cs->sb.data_count + BYTES_PER_SAMPLE > ARRAY_SIZE(cs->sb.data)) + sensor_bulk_report(&cs->sb, oid); +} + +static void +cs1237_read_adc(struct cs1237_adc *cs, uint8_t oid) +{ + uint8_t old_flags = cs->flags; + uint32_t raw; + + if (old_flags & CS_CONFIG_PENDING) { + // Read and discard the sample produced with the power-on settings. + raw = cs1237_read_bits(cs, 24); + cs1237_write_config(cs); + } else { + // Bits 25-27 are update1, reserved zero, and forced high. + raw = cs1237_read_bits(cs, 27); + } + + irq_disable(); + old_flags |= cs->flags; + cs->flags &= ~(CS_PENDING | CS_OVERFLOW); + if (old_flags & CS_CONFIG_PENDING) { + cs->flags &= ~CS_CONFIG_PENDING; + cs->flags |= CS_CONFIG_VERIFY; + } + irq_enable(); + + if (old_flags & CS_CONFIG_PENDING) + return; + + uint_fast8_t status = raw & 0x07; + uint32_t counts = raw >> 3; + if (counts & 0x800000) + counts |= 0xff000000; + + uint_fast8_t error = 0; + if ((status & 0x03) != 0x01) + error = SE_DESYNC; + else if ((old_flags & CS_CONFIG_VERIFY) && !(status & 0x04)) + error = SE_CONFIG; + else if (old_flags & CS_OVERFLOW) + error = SE_READ_TOO_LONG; + + if (old_flags & CS_CONFIG_VERIFY) { + irq_disable(); + cs->flags &= ~CS_CONFIG_VERIFY; + if (error) + cs->flags |= CS_CONFIG_PENDING; + else + cs->is_configured = 1; + irq_enable(); + } + if (error) { + counts = (uint32_t)error << 24; + } else if (cs->lce) { + // probe is optional, report if enabled + load_cell_probe_report_sample(cs->lce, counts); + } + add_sample(cs, oid, counts); +} + +void +command_config_cs1237(uint32_t *args) +{ + struct cs1237_adc *cs = oid_alloc(args[0], command_config_cs1237, + sizeof(*cs)); + cs->timer.func = cs1237_event; + cs->config = args[1] & 0x7f; + cs->dout = gpio_in_setup(args[2], 0); + cs->dout_out = gpio_out_setup(args[2], 0); + gpio_in_reset(cs->dout, 0); + cs->sclk = gpio_out_setup(args[3], 0); + gpio_out_write(cs->sclk, 1); +} +DECL_COMMAND(command_config_cs1237, "config_cs1237 oid=%c config=%c" + " dout_pin=%u sclk_pin=%u"); + +void +cs1237_attach_load_cell_probe(uint32_t *args) +{ + struct cs1237_adc *cs = oid_lookup(args[0], command_config_cs1237); + cs->lce = load_cell_probe_oid_lookup(args[1]); +} +DECL_COMMAND(cs1237_attach_load_cell_probe, "cs1237_attach_load_cell_probe" + " oid=%c load_cell_probe_oid=%c"); + +void +command_query_cs1237(uint32_t *args) +{ + struct cs1237_adc *cs = oid_lookup(args[0], command_config_cs1237); + sched_del_timer(&cs->timer); + cs->flags = 0; + cs->rest_ticks = args[1]; + if (!cs->rest_ticks) { + gpio_out_write(cs->sclk, 1); + return; + } + gpio_in_reset(cs->dout, 0); + gpio_out_write(cs->sclk, 0); + if (!cs->is_configured) + cs->flags = CS_CONFIG_PENDING; + sensor_bulk_reset(&cs->sb); + irq_disable(); + cs->timer.waketime = timer_read_time() + cs->rest_ticks; + sched_add_timer(&cs->timer); + irq_enable(); +} +DECL_COMMAND(command_query_cs1237, "query_cs1237 oid=%c rest_ticks=%u"); + +void +command_query_cs1237_status(const uint32_t *args) +{ + uint8_t oid = args[0]; + struct cs1237_adc *cs = oid_lookup(oid, command_config_cs1237); + irq_disable(); + const uint32_t start_t = timer_read_time(); + uint8_t ready = cs1237_is_data_ready(cs); + irq_enable(); + sensor_bulk_status(&cs->sb, oid, start_t, 0, + ready ? BYTES_PER_SAMPLE : 0); +} +DECL_COMMAND(command_query_cs1237_status, "query_cs1237_status oid=%c"); + +void +cs1237_capture_task(void) +{ + if (!sched_check_wake(&wake_cs1237)) + return; + uint8_t oid; + struct cs1237_adc *cs; + foreach_oid(oid, cs, command_config_cs1237) { + if (cs->flags & CS_PENDING) + cs1237_read_adc(cs, oid); + } +} +DECL_TASK(cs1237_capture_task); diff --git a/test/configs/ar100.config b/test/configs/ar100.config index 8f0a225b97..f8eca0f1bb 100644 --- a/test/configs/ar100.config +++ b/test/configs/ar100.config @@ -8,5 +8,6 @@ CONFIG_WANT_LIS2DW=n CONFIG_WANT_HX71X=n CONFIG_WANT_ADS1220=n CONFIG_WANT_ADS131M0X=n +CONFIG_WANT_CS1237=n CONFIG_WANT_LOAD_CELL_PROBE=n CONFIG_WANT_SENSOR_ANGLE=n diff --git a/test/configs/stm32f031.config b/test/configs/stm32f031.config index 26b7b0c4e0..259ea44ec6 100644 --- a/test/configs/stm32f031.config +++ b/test/configs/stm32f031.config @@ -10,5 +10,6 @@ CONFIG_WANT_LIS2DW=n CONFIG_WANT_LDC1612=n CONFIG_WANT_HX71X=n CONFIG_WANT_ADS1220=n +CONFIG_WANT_CS1237=n CONFIG_WANT_LOAD_CELL_PROBE=n CONFIG_WANT_ICM20948=n diff --git a/test/configs/stm32f042.config b/test/configs/stm32f042.config index c1ca306f2a..0e54decd1a 100644 --- a/test/configs/stm32f042.config +++ b/test/configs/stm32f042.config @@ -7,4 +7,5 @@ CONFIG_WANT_LDC1612=n CONFIG_WANT_HX71X=n CONFIG_WANT_ADS1220=n CONFIG_WANT_ADS131M0X=n +CONFIG_WANT_CS1237=n CONFIG_WANT_ICM20948=n diff --git a/test/klippy/load_cell.cfg b/test/klippy/load_cell.cfg index 03a3f1b85d..6434c40de8 100644 --- a/test/klippy/load_cell.cfg +++ b/test/klippy/load_cell.cfg @@ -17,3 +17,7 @@ dout_pin: PA3 sensor_type: hx717 sclk_pin: PA4 dout_pin: PA5 +[load_cell my_cs1237] +sensor_type: cs1237 +sclk_pin: PA6 +dout_pin: PA7