Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions docs/Load_Cell.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_\
Expand Down Expand Up @@ -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 `^`).
5 changes: 3 additions & 2 deletions klippy/extras/load_cell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
246 changes: 246 additions & 0 deletions klippy/extras/load_cell/cs1237.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# CS1237 ADC load cell sensor support
#
# Copyright (C) 2026 OpenCentauri Contributors
# Modelled on hx71x.py by Gareth Farrington <gareth@waves.ky> and
# rewritten for Kalico's tap interface by James Turton <james.turton@gmx.com>.
# Drop-in alternative to hx71x for use with [load_cell] tap interface.
#
# Usage (printer.cfg):
#
# [load_cell]
# sensor_type: cs1237
# drdy_pin: <pin> # DRDY / data pin
# sclk_pin: <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, "<i")
self.batch_bulk = bulk_sensor.BatchBulkHelper(
self.printer,
self._process_batch,
self._start_measurements,
self._finish_measurements,
UPDATE_INTERVAL,
)

# ---- MCU command handles ----
self.query_cs1237_cmd = None
self.config_cs1237_cmd = None
self.attach_probe_cmd = None
mcu.add_config_cmd(
"config_cs1237 oid=%d drdy_pin=%s sclk_pin=%s"
% (
self.oid,
self.drdy_pin,
self.sclk_pin,
)
)
mcu.add_config_cmd(
"query_cs1237 oid=%d rest_ticks=0" % (self.oid,),
on_restart=True,
)
mcu.register_config_callback(self._build_config)

# ---- LoadCellSensor interface ----

def get_mcu(self) -> 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}
5 changes: 3 additions & 2 deletions klippy/extras/load_cell_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
# Copyright (C) 2025 Gareth Farrington <gareth@waves.ky>
#
# 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


# 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)

Expand Down
12 changes: 10 additions & 2 deletions src/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading