Code: github.com/WianStipp/hx711

I walk through building a miniature load-cell scale: bit-banging the HX711 from a Raspberry Pi Pico, two-point linear calibration, and live weight on a Waveshare 1.14" LCD.

Raspberry Pi Pico with Waveshare 1.14 inch LCD showing the weighing scale UI

The idea

I love making pour over coffee (La Cabra) and precisely weighing beans is key to consistent cups. However, precision weighing scales are expensive and has made previous roommates question my true occupation. So instead, let's build one for an even higher cost and simultaneously look even more dodgy.

Coffee bean weighing scale prototype

A load cell outputs a tiny differential voltage proportional to force. The HX711 is a 24-bit ADC that amplifies that signal and hands the MCU a serial stream of counts, not grams. Those counts are offset by mechanical preload, amplifier gain, and wiring, so raw ADC values are useless until you map them onto a known mass. The prototype clocks out each 24-bit reading over GPIO, applies a precomputed two-point calibration (empty scale vs a known weight), and paints the result on the LCD. Later goals: cleaner hardware layout, tare, and Wi‑Fi via a Pico-ESP8266.

The mechanism

With an empty reading r0r_0 and a known-mass reading rwr_w at mass ww, the slope (counts per gram) is:

c=r0rwwc = \frac{r_0 - r_w}{w}

A new reading rr converts to mass by reversing that map:

m^=r0rc\hat{m} = \frac{r_0 - r}{c}

In the prototype this is hardcoded as:

r0=203100,rw=570000,w=461gc=r0rww,m^=r0rc\begin{align*} r_0 &= 203100, \quad r_w = 570000, \quad w = 461\,\mathrm{g} \\ c &= \frac{r_0 - r_w}{w}, \quad \hat{m} = \frac{r_0 - r}{c} \end{align*}
Two-point calibration from empty reading to known mass
Figure 1: Two-point calibration. Empty (0,r0)(0, r_0) and known mass (w,rw)(w, r_w) fix the line; a new reading rr maps to estimated mass m^\hat{m}.

The HX711 protocol itself is clock-driven: hold SCK low until DOUT falls (conversion ready), then pulse SCK 24 times to shift out the signed sample MSB-first. Extra SCK pulses after the sample select channel and gain for the next conversion.

Worth knowing

  • Calibration constants are hardware-specific: change the load cell, wiring, or gain and you must remeasure r0r_0 and rwr_w.
  • Sharing ground between the Pico-LCD and HX711 matters; the Waveshare board needs a daisy-chained ground or the ADC readings become noisy/unreliable.
  • Tare is just a runtime update of r0r_0 (current empty reading), not a change to the slope cc—unless temperature or mechanical creep shifts the gain.
  • Averaging several readings before display kills flicker; a single 24-bit sample is already quantized finely but still noisy at gram-scale use.

Code

The live loop lives in prototyping/main.py: pulse the clock, assemble 24 bits, convert with the two-point factors, redraw the LCD.

ZERO_READING = 203100
KNOWN_READING = 570000
KNOWN_WEIGHT = 461
CONVERSION_FACTOR = (ZERO_READING - KNOWN_READING) / KNOWN_WEIGHT

def read_hx711():
    clock_pin.value(1)
    clock_pin.value(0)

    while data_pin.value() == 1:
        pass

    data = 0
    for _ in range(24):
        clock_pin.value(1)
        data = (data << 1) | data_pin.value()
        clock_pin.value(0)

    for _ in range(64):  # post-sample SCK pulses (channel/gain)
        clock_pin.value(1)
        clock_pin.value(0)

    return (ZERO_READING - data) / CONVERSION_FACTOR

Further reading