HX711 support (load cell sensor)

Support for the HX711 load cell sensor chip has been requested a number of times already. I have written a preliminary module which is able to read out data from the HX711. It works without modifying the MCU code, but there is a catch: It does not obey the specifications in the HX711 data sheet.

The HX711 has a unidirectional SPI-like interface with a clock and a data bin. The host is expected to read 24 bits after the HX711 has send a data ready signal by pulling the data pin to low. After the transfer, it should send 1-3 additional clock signals to configure the gain for the next conversion.

My module simply reads 32 bits periodically (currently with ~10 Hz, as my HX711 module is configured to that frequency apparently). The additional clock signals seem to be ignored (although the data sheet explicitly mentions to send no more than 27 clock pulses per conversion interval). The gain will then presumably fixed at 64 (lowest setting).

More problematic is actually ignoring the data ready signal. With a logic analyzer I have found that the HX711 actually revokes the data ready signal if the data has not been read out before the end of the conversion interval. If the host attempts to read the data during that time it probably will read simply -1 (all bits high).

Another issue is that the SPI implementation in Klipper seems to expect always all 4 pins to be configured.

Anyway, this is a first starting point I wanted to share with the community. Maybe some people have ideas how to improve this without the need for special microcontroller code (which in principle would be fine for me, but I think it would be nice to avoid it).

So my first goal to improve this would be to read out the data ready signal. Is it possible to read out a pin which is configured as a soft-SPI MISO pin in addition as a plain GPIO input pin? If yes, is it realistic to poll this pin with a frequency (much) bigger than 80 Hz (the fastest possible HX711 read out frequency) and react to it in time with initiating a transfer?

In the mean time, I got some ideas on my own how to improve that, at least without writing HX711-specific microcontroller code. This would require some new features in the microcontroller and communication protocol though:

  • Scheduled SPI communication, i.e. the ability to control at which time an SPI read shall take place. Ideally, several reads can be queued at the same time to achieve 80 Hz read rate.
  • Soft-SPI with arbitrary bit width, i.e. transfer e.g. 25 bits (rather than 24 or 32).
  • Pin change interrupt recorder with dead time, i.e. allow to enable the pin change interrupt for a specified pin and transport the time stamp when such interrupt arrives to the Python code. A configurable dead time should prevent the interrupt to be recorded again within the dead time period after it has been recorded already (this limits the rate).
  • Allow duplicate assignment of pins in certain situations, specifically the MISO pin of a soft-SPI must be used for the pin change interrupt recorder at the same time (the dead time will prevent recording the SPI data as pin changes). I guess, the pin change interrupt recorder should simply be usable for all inputs, even if already taken by some other driver.
  • Allow soft SPI without specifying CS and MOSI pins.

The idea is that the HX711 operates at a fixed conversion interval (either 10 Hz or 80 Hz, selectable via hardware pin - usually this is done as a soldering option for off-the-shelf HX711 modules). The exact interval merely needs to be synchronised to the Klipper main clock. If that is achieved, the transfers can simply be scheduled at the right time. The synchronisation of the interval can be done via the pin change interrupt. We have to observe at which time the data pin is pulled to low (outside an SPI transfer). The transfer should be scheduled shortly after that time. Since the interval is roughly known, it should be enough to adapt the phase of the newly scheduled transfers whenever the pin change interrupt is seen by the Python code.

I think these additional features might be useful for other things as well. I think I can implement this within a few weeks or so, but I would like to hear an opinion first, if it sounds reasonable. I could also implement it as a special driver in the microcontroller (just like it is done for the ADXL345), but this would be special code which cannot help other projects.

Scheduled transfers would also help the load cell-based probe algorithm to speed up the measurements. My current hardware does not use a HX711 there, instead I have an I2C-attached ADC. Hence I might try to implement scheduled I2C transfers along with the SPI.

FWIW, I suspect a simple sensor_hx711.c module will be easier to implement and maintain than an “arbitrary pseudo-spi interface”. My experience has been that all of these “quriky spi” devices are sufficiently different from the next that no appreciable code reuse is possible.

It’s already possible to use SPI without a CS pin (both soft-spi and regular hardware SPI). There’s been a few requests of being able to implement SPI without a MOSI or MISO pin (the former for MAX6675 temperature sensor and the latter for UC1701 and similar displays). The suggestion in the past was to introduce a low-level DUMMY mcu gpio pin that ignores updates. No one has gotten around to implementing this though. I suspect most people just designate an otherwise unused gpio pin to be used as a dummy pin.

-Kevin

Ok, I will aim in that direction then. Surly it will be easier to implement, I just wasn’t sure if more chip-specific code in the microcontroller code would be welcome…

HI, where is the code of your implementation?

I’m considering doing something related but RPI2040-specific, where the actual HX711 interface code is implemented by the 2040 state machine - which would be very fast. The code for that already exists.
It would only run on the 2040, but then, these things are cheap.

WDYT?

I did not yet write an MCU-based implementation. Someone else anyway has started this already, see this pull request:

I have improved my temporary (!) Python-side implementation though, and it seems to be pretty reliable right now. It uses out-of-spec tricks though, so maybe different batches of the chip might work differently. Also it is limited to maximum gain (because the standard SPI implementation cannot do 25, 26 or 27 bits - I just always read 32 which is already out of spec). To synchronise with the sampling interval of the chip, I rely on receiving 0 if readout is started before the new sample is available. This is also out-of-spec, and I do not know if it works under all conditions, but so far I have seen no issues. This of course means the value 0 can never be seen, but the readout at maximum gain is anyway so noisy that this usually does not matter much :wink: Finally, the code is probably only working with the 10 Hz configuration. That code can be found here:

I’m considering doing something related but RPI2040-specific, where the actual HX711 interface code is implemented by the 2040 state machine - which would be very fast.

I don’t think it is worth using the RP2040 state machine for this purpose. Speed is not an issue. We have at most 80 SPS. The serial interface is synchronous, so bit banging is no problem. People may want to connect the HX711 to a normal 3D printer board directly. Adding another microcontroller just to read out the HX711 is IMO not nice. Finally I see potential to do better things with the state machine in a 3D printer (someone ever thought to write a special peripheral to generate the motor steps super precisely and fast?)… :wink:

Hmmm ok, but notice that in order to mount the bed on load cells, I’d need to query 3 HX711s

This is still not a big deal. Especially if you anyway connect them to a dedicated MCU, you can probably read out 100s of HX711 without problems on a modern microcontroller. The RP2040 has only 2 programmable IOs, so you can’t even query 3 HX711 with that (unless you want to implement support for multiple HX711 in a single PIO).

Besides: You can connect load cells in parallel to the same HX711, if you are only interested in the average force. I also have two load cells connected in parallel.