HX711 based balance

About the post

With an HX711 module and a load cell, it is easy to build an amazingly exact scale. In this article, I would like to show how this works in principle.  

My goal was to achieve the same functionality as my digital kitchen scale: 

  • Output of the weight on a display
  • Only one button to turn on and for tare
  • Automatic shutdown after a certain time

The article is structured as follows:

What you need

Typical kit of load cell and HX711 module
Typical kit of load cell and HX711 module

To build the scale, you need a load cell and an HX711 module. You can buy both as a kit. If you search for “HX711 load cell” on Amazon or eBay, for example, you’ll get dozens of offers for a few euros. Select a load cell with the weight range that is right for you. For this post, I chose a 2 kg cell. 

That being said, I can’t provide you with a detailed shopping list because it depends too much on your specific wants and needs. I recommend reading first this article and then decide for yourself.

The measuring principle

The load cell is slightly bent by the weight of the load to be weighed. Under the white glue there is a strain gauge, which resistance value changes with the degree of elongation, i.e. with the weight. The resistance generates a voltage drop which in turn is evaluated with an A/D converter. 

The change in voltage drop with increasing weight is small. I measured a few millivolts per kilogram. The A/D converter of an Arduino UNO or an ATmega328P is not suitable for this purpose. The HX711, on the other hand, has an impressive resolution of 24 bits (= 16,777,216).

The HX711 itself is actually the sixteen pin chip on the module. Since it still needs a few parts to work properly, it is convenient to use the ready-to-use module. If you are interested in more technical details of the HX711, you find a data sheet here.

Installing the load cell

You can’t avoid some tinkering. The load cell must be installed in a way that it will bend under weight. I just took a board which I found in my cellar and sawed off two equally sized pieces. I placed the load cell between the boards with spacers.

The load cell with spacers between two boards
The load cell with spacers between two boards

I drilled two holes through the bottom board and attached the load cell with two M5 screws. In addition, I added some rubber spacers to the underside. 

Underside of the HX711 scale
Underside of the scale

For the other side, M4 screws are actually provided. However, in order to have a smooth support surface for the weighing material, I did not want to pierce the upper board. That’s why I just took a few wood screws.  

Screws chosen by me
Screws chosen by me

Connect the load cell cables to the module as follows:

  • Red on E+
  • Black on E-
  • White on A-
  • Green on E+
Connection of the load cell to the HX711 module
Connection of the load cell to the HX711 module

This is the interim status:

The HX711 based balance in raw condition
The balance in raw condition

Using the HX711 ADC Library

I used Olav Kallhovd’s HX711 ADC library to control the module. You can download it directly from Github here or install it via the Arduino IDE library manager. 

HX711 Basic wiring

Minimum connection of the HX711 to the Arduino UNO
Minimum connection of the HX711 to the Arduino UNO

The module can be operated with voltages between 2.6 and 5.5 volts. The module is < quite economical. Its power consumption is 1.5 milliamperes. The DT pin is connected to the Arduino Pin 4, SCK is attached to pin 5. You can choose other pins if you want. 

Calibration of the balance

To calibrate, you need an object with a known weight. The weight should be exactly known to the gram and not too small. It is best to take a second balance. 

The beauty of the library used here is that it makes life – or at least the balance calibration ­čśë – easy for you. Select the “Calibration.ino” sketch from the library’s examples. I have printed it here unchanged:

//-------------------------------------------------------------------------------------
// HX711_ADC.h
// Arduino master library for HX711 24-Bit Analog-to-Digital Converter for Weigh Scales
// Olav Kallhovd sept2017
// Tested with      : HX711 asian module on channel A and YZC-133 3kg load cell
// Tested with MCU  : Arduino Nano, ESP8266
//-------------------------------------------------------------------------------------
// This is an example sketch on how to use this library
// Settling time (number of samples) and data filtering can be adjusted in the config.h file

// This example shows how to calibrate the load cell and optionally save the calibration  
// value to EEPROM, and also how to change the value.
// The value can later be fetched from EEPROM in your project sketch.

#include <HX711_ADC.h>
#include <EEPROM.h>

//HX711 constructor (dout pin, sck pin):
HX711_ADC LoadCell(4, 5);

int eepromAdress = 0;

unsigned long t = 0;

void calibrate() {
  Serial.println("***");
  Serial.println("Start calibration:");
  Serial.println("It is assumed that the mcu was started with no load applied to the load cell.");
  Serial.println("Now, place your known mass on the loadcell,");
  Serial.println("then send the weight of this mass (i.e. 100.0) from serial monitor.");
  float m = 0;
  boolean f = 0;
  while (f == 0) {
    LoadCell.update();
    if (Serial.available() > 0) {
      m = Serial.parseFloat();
      if (m != 0) {
        Serial.print("Known mass is: ");
        Serial.println(m);
        f = 1;
      }
      else {
        Serial.println("Invalid value");
      }
    }
  }
  float c = LoadCell.getData() / m;
  LoadCell.setCalFactor(c);
  Serial.print("Calculated calibration value is: ");
  Serial.print(c);
  Serial.println(", use this in your project sketch");
  f = 0;
  Serial.print("Save this value to EEPROM adress ");
  Serial.print(eepromAdress);
  Serial.println("? y/n");
  while (f == 0) {
    if (Serial.available() > 0) {
      char inByte = Serial.read();
      if (inByte == 'y') {
        #if defined(ESP8266) 
        EEPROM.begin(512);
        #endif
        EEPROM.put(eepromAdress, c);
        #if defined(ESP8266)
        EEPROM.commit();
        #endif
        EEPROM.get(eepromAdress, c);
        Serial.print("Value ");
        Serial.print(c);
        Serial.print(" saved to EEPROM address: ");
        Serial.println(eepromAdress);
        f = 1;

      }
      else if (inByte == 'n') {
        Serial.println("Value not saved to EEPROM");
        f = 1;
      }
    }
  }
  Serial.println("End calibration");
  Serial.println("For manual edit, send 'c' from serial monitor");
  Serial.println("***");
}

void changeSavedCalFactor() {
  float c = LoadCell.getCalFactor();
  boolean f = 0;
  Serial.println("***");
  Serial.print("Current value is: ");
  Serial.println(c);
  Serial.println("Now, send the new value from serial monitor, i.e. 696.0");
  while (f == 0) {
    if (Serial.available() > 0) {
      c = Serial.parseFloat();
      if (c != 0) {
        Serial.print("New calibration value is: ");
        Serial.println(c);
        LoadCell.setCalFactor(c);
        f = 1;
      }
      else {
        Serial.println("Invalid value, exit");
        return;
      }
    }
  }
  f = 0;
  Serial.print("Save this value to EEPROM adress ");
  Serial.print(eepromAdress);
  Serial.println("? y/n");
  while (f == 0) {
    if (Serial.available() > 0) {
      char inByte = Serial.read();
      if (inByte == 'y') {
        #if defined(ESP8266)
        EEPROM.begin(512);
        #endif
        EEPROM.put(eepromAdress, c);
        #if defined(ESP8266)
        EEPROM.commit();
        #endif
        EEPROM.get(eepromAdress, c);
        Serial.print("Value ");
        Serial.print(c);
        Serial.print(" saved to EEPROM address: ");
        Serial.println(eepromAdress);
        f = 1;
      }
      else if (inByte == 'n') {
        Serial.println("Value not saved to EEPROM");
        f = 1;
      }
    }
  }
  Serial.println("End change calibration value");
  Serial.println("***");
}

void setup() {
  Serial.begin(9600); delay(10);
  Serial.println();
  Serial.println("Starting...");
  LoadCell.begin();
  long stabilisingtime = 2000; // tare preciscion can be improved by adding a few seconds of stabilising time
  LoadCell.start(stabilisingtime);
  if (LoadCell.getTareTimeoutFlag()) {
    Serial.println("Tare timeout, check MCU>HX711 wiring and pin designations");
  }
  else {
    LoadCell.setCalFactor(1.0); // user set calibration value (float)
    Serial.println("Startup + tare is complete");
  }
  while (!LoadCell.update());
  calibrate();
}
void loop() {
  //update() should be called at least as often as HX711 sample rate; >10Hz@10SPS, >80Hz@80SPS
  //longer delay in sketch will reduce effective sample rate (be carefull with delay() in the loop)
  LoadCell.update();

  //get smoothed value from the data set
  if (millis() > t + 250) {
    float i = LoadCell.getData();
    Serial.print("Load_cell output val: ");
    Serial.println(i);
    t = millis();
  }

  //receive from serial terminal
  if (Serial.available() > 0) {
    char inByte = Serial.read();
    if (inByte == 't') LoadCell.tareNoDelay();
    else if (inByte == 'c') changeSavedCalFactor();
  }

  //check if last tare operation is complete
  if (LoadCell.getTareStatus() == true) {
    Serial.println("Tare complete");
  }

}

 

 

Start the sketch and open the serial monitor. Wait until the following message is displayed:

Carriage calibration on the serial monitor
Balance calibration on the serial monitor, input of the known weight

Then take the weight, put it on the scale, enter the weight in grams and press Enter or click on Send. Write down the “calibration value” or have it written in the EEPROM of the Arduino UNO. The balance is now calibrated and provides the weight.

The scale in regular operation
Result of the balance calibration

If the weight drifts a little, then repeat the calibration and try a longer “stabilizing time” (line 145).  

Regular operation on the PC
My “calibration measure”

Regular operation of the HX711 balance

After calibrating the scale, you can now go into regular operation. The sketch “Read_1x_load_cell.ino” is a good starting point for this. You only have to enter your calibration factor in line 41 or – if you have it in the EEPROM – uncomment line 45. Otherwise, the sketch works “out of the box”. If you are using an ESP8266, you will also need to uncomment on line 43.

For the tare you enter a “t” in the serial monitor.

/*
   -------------------------------------------------------------------------------------
   HX711_ADC
   Arduino library for HX711 24-Bit Analog-to-Digital Converter for Weight Scales
   Olav Kallhovd sept2017
   -------------------------------------------------------------------------------------
*/

/*
   Settling time (number of samples) and data filtering can be adjusted in the config.h file
   For calibration and storing the calibration value in eeprom, see example file "Calibration.ino"

   The update() function checks for new data and starts the next conversion. In order to acheive maximum effective
   sample rate, update() should be called at least as often as the HX711 sample rate; >10Hz@10SPS, >80Hz@80SPS.
   If you have other time consuming code running (i.e. a graphical LCD), consider calling update() from an interrupt routine,
   see example file "Read_1x_load_cell_interrupt_driven.ino".

   This is an example sketch on how to use this library
*/

#include <HX711_ADC.h>
#include <EEPROM.h>

//pins:
const int HX711_dout = 4; //mcu > HX711 dout pin
const int HX711_sck = 5; //mcu > HX711 sck pin

//HX711 constructor:
HX711_ADC LoadCell(HX711_dout, HX711_sck);

const int calVal_eepromAdress = 0;
long t;

void setup() {
  Serial.begin(57600); delay(10);
  Serial.println();
  Serial.println("Starting...");

  LoadCell.begin();
  float calibrationValue; // calibration value (see example file "Calibration.ino")
  calibrationValue = 696.0; // uncomment this if you want to set the calibration value in the sketch
#if defined(ESP8266)|| defined(ESP32)
  //EEPROM.begin(512); // uncomment this if you use ESP8266/ESP32 and want to fetch the calibration value from eeprom
#endif
  //EEPROM.get(calVal_eepromAdress, calibrationValue); // uncomment this if you want to fetch the calibration value from eeprom

  long stabilizingtime = 2000; // preciscion right after power-up can be improved by adding a few seconds of stabilizing time
  boolean _tare = true; //set this to false if you don't want tare to be performed in the next step
  LoadCell.start(stabilizingtime, _tare);
  if (LoadCell.getTareTimeoutFlag()) {
    Serial.println("Timeout, check MCU>HX711 wiring and pin designations");
    while (1);
  }
  else {
    LoadCell.setCalFactor(calibrationValue); // set calibration value (float)
    Serial.println("Startup is complete");
  }
}

void loop() {
  static boolean newDataReady = 0;
  const int serialPrintInterval = 0; //increase value to slow down serial print activity

  // check for new data/start next conversion:
  if (LoadCell.update()) newDataReady = true;

  // get smoothed value from the dataset:
  if (newDataReady) {
    if (millis() > t + serialPrintInterval) {
      float i = LoadCell.getData();
      Serial.print("Load_cell output val: ");
      Serial.println(i);
      newDataReady = 0;
      t = millis();
    }
  }

  // receive command from serial terminal, send 't' to initiate tare operation:
  if (Serial.available() > 0) {
    float i;
    char inByte = Serial.read();
    if (inByte == 't') LoadCell.tareNoDelay();
  }

  // check if last tare operation is complete:
  if (LoadCell.getTareStatus() == true) {
    Serial.println("Tare complete");
  }

}

Maybe you don’t want to tare the balance when you switch it on? Then, in line 48, change the value of _tare to false. You will then get a “fantasy value” as a measurement result. Write down this value and subtract it from future measurement results. You have frozen the tare, so to speak.

I then tested the HX711 based balance against my kitchen balance….

It's coming up: my kitchen scales...
It’s coming up: my kitchen scales…
... against the self-built HX711 construction.
… against the self-built HX711 construction.

… and the results were right on the gram!

Output via an OLED display

To make the scale independent of the PC, you need an output medium. I have selected a small OLED display that requires only a few milliamperes of power. It is controlled via I2C using the libraries Adafruit GFX and Adafruit SSD1306. If you want to do the same, you can download the Github libraries from the links or install them via the library manager of the Arduino IDE. I do not go into the details here because that would go beyond the scope. In addition, you might want to use completely different displays or seven-segment displays.

Output via a TFT display
Output via a TFT display

A few notes on the following sketch. I put a bit of effort into displaying the weight right-aligned. This is done in the floatToDisplayString function. First, the function determines the number of blanks and then attaches the weight as a string. 

Tare measurements are requested via interrupts at pin 2. The interrupt is triggered by a button. When the button is pressed, the variable taraRequest turns true and thus a tare measurement is initiated in the main loop. During tare measurement, the balance outputs “Wait” on the display. 

The rest of the sketch should be reasonably self-explanatory (of course, everyone claims that for their code…). If you have questions, ask!

#include <Wire.h>
#include <HX711_ADC.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define OLED_RESET 7 // we don't have a reset, but the constructor expects it 

Adafruit_SSD1306 display(OLED_RESET);
HX711_ADC LoadCell(4, 5);

byte interruptPin=2;
volatile bool taraRequest = false;

void setup()   {                
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), taraEvent, RISING);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x64)
  display.clearDisplay();
  display.setTextSize(4);
  display.setTextColor(WHITE);
  display.setCursor(10,4);
  display.println("Wait");
  display.display();
  LoadCell.begin();
  LoadCell.start(2000); 
  LoadCell.setCalFactor(883.73); 
}


void loop() {
  float weightAsFloat = 0.0;
  unsigned long t = 0;
  
  LoadCell.update();
  
  if (millis() > t + 250) {
    weightAsFloat = LoadCell.getData();
    displayWeight(weightAsFloat);
    t = millis();  
  }
  if(taraRequest){
    doTara();
    taraRequest = false;
  }
}

void displayWeight(float weight){
  String weightAsString = "";
  weightAsString = floatToDisplayString(weight);
  display.clearDisplay();
  display.setCursor(0,4);
  display.println(weightAsString);
  display.display();
}

void doTara(){
  LoadCell.tareNoDelay();
  display.clearDisplay();
  display.setCursor(10,4);
  display.println("Wait");
  display.display();
  while(LoadCell.getTareStatus()== false){
    LoadCell.update();
    delay(50);
  }
}

void taraEvent(){
  taraRequest = true;
}

String floatToDisplayString(float floatValue){
  String stringValue=" ";
  int intValue = (int)(round(floatValue));
  if(intValue<0){
    stringValue="";
  }
  uint8_t blanks = 3 - int(log10(abs(intValue)));
  for(int i=0; i<blanks; i++){
    stringValue+=" ";
  }
  stringValue+=(String)intValue;
  return stringValue; 
}

 

Here’s what it looks like:

HX711 balance with OLED display and tare button
OLED display and tare button

The next evolutionary step: The scale goes to sleep

Then I wanted the scale and its components to go into standby mode when there is no change in weight or a new tare measurement being requested for some time. Since the Arduino UNO still consumes a lot of power in sleep mode (see my penultimate post about Sleep Modes), I used the bare ATmega328P here. I have described here how to program the ATmega328P with the Arduino IDE. The circuit for this is as follows: 

Complete circuit for the scale with display and tare button
Complete circuit for the scale with display and tare button

In the sketch for this variant, I introduced the variable lastWeightAsFloat that stores the value of the last measurement. This value is compared to the current result. As long as the last and the current weight differ (difference < 1 g), the balance is obviously busy. And during that time the variable tLastChange is repeatedly updated to millis . The same happens during tare measurement. Once per main loop tLastChange is being compared with millis . If the difference exceeds 120,000 (= 2 min), the balance is sent to sleep. First the display is switched off, then the HX711 module and finally the ATmega328P is put into deep sleep mode.

An interrupt awakens the ATmega328P. Since the button for tare measurement triggers an interrupt, it also acts as awake-up function.

#include <Wire.h>
#include <HX711_ADC.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <avr/sleep.h>
#define OLED_RESET 9 // we don't have a reset, but the constructor expects it 

Adafruit_SSD1306 display(OLED_RESET);
HX711_ADC LoadCell(4, 5);

int interruptPin = 2;   // tara and wake-up pin 
int powerOnPin = 6;
volatile bool taraRequest = false;
float weightAsFloat = 0.0; // current weight (as float)
float lastWeightAsFloat = 9999.0;  // former weight
unsigned long t = 0;    // system time of last weight measurement 
unsigned long tLastChange = 0;  // system time of last change of weight

void setup()   {  
  pinMode(interruptPin, INPUT);
  pinMode(powerOnPin, OUTPUT);
  digitalWrite(powerOnPin, HIGH);
  initBalance();
  attachInterrupt(digitalPinToInterrupt(interruptPin), taraEvent, RISING);
}

void loop() {
  LoadCell.update();
  /* In one loop a) measurement is done or b) a tara or c) the balance will be send to sleep
  or d) nothing happens */
  if (millis() > (t + 250)) {
    weightAsFloat = LoadCell.getData();
    displayWeight(weightAsFloat);
    if(abs(weightAsFloat-lastWeightAsFloat) >=1){
      tLastChange = millis();
      lastWeightAsFloat = weightAsFloat;  
    }
    t = millis();
  }
  
  if(taraRequest){
    doTara();
    taraRequest = false;
  }
  
  if(millis() > (tLastChange + 120000)){ // after 2 min of no weight change or tara the balance shall fall asleep
    sleepAndWakeUp();
  }
}

void initBalance(){
  taraRequest = false;
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x64)
  display.clearDisplay();
  display.setTextSize(4);
  display.setTextColor(WHITE);
  display.setCursor(10,4);
  display.println("Wait");
  display.display();
  LoadCell.begin();
  LoadCell.start(2000); 
  LoadCell.setCalFactor(883.73); 
  weightAsFloat = 0.0;
  lastWeightAsFloat = 9999.0;
  t = 0;
}

void displayWeight(float weight){
  String weightAsString = "";
  weightAsString = floatToDisplayString(weight);
  display.clearDisplay();
  display.setCursor(0,4);
  display.println(weightAsString);
  display.display();
}

/* The following function displays the weight. The weight is therefore
 * changed into a string. The weight shall be displaey on the right side,
 * therefore the string begins with blanks.  
 */
String floatToDisplayString(float floatValue){
  String stringValue=" ";
  int intValue = (int)(round(floatValue));
  if(intValue<0){
    stringValue="";
  }
  uint8_t blanks = 3 - int(log10(abs(intValue)));
  for(int i=0; i<blanks; i++){
    stringValue+=" ";
  }
  stringValue+=(String)intValue;
  return stringValue; 
}

void doTara(){    // tara
  LoadCell.tareNoDelay();
  display.clearDisplay();
  display.setCursor(10,4);
  display.println("Wait");
  display.display();
  while(LoadCell.getTareStatus()== false){
    LoadCell.update();
    delay(50);
  }
  tLastChange = millis();
}

void taraEvent(){
  taraRequest = true;
}

void sleepAndWakeUp(){
  LoadCell.powerDown();            // switch off HX711
  display.ssd1306_command(SSD1306_DISPLAYOFF);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // deep sleep mode
  cli();
  sleep_enable();
  sleep_bod_disable(); // disable brown-out detector
  sei();
  sleep_cpu();
  /* ATmega328P sleeps */
  sleep_disable();
  LoadCell.powerUp();            // switch on HX711
  display.ssd1306_command(SSD1306_DISPLAYON);
  initBalance();
}

 

With this setup, I have measured a power consumption of 139 microamperes in sleep mode. That’s 3.3 milliampere hours per day. So a battery should last for some time.

Turn off the balance completely

Then I thought about how to turn off the scale completely without installing an additional switch (because everyone can!). Strictly speaking, the question is how the scale can turn itself off.

I solved the task with a thyristor. A thyristor has some similarities with a transistor. It has three connections, namely the cathode, the anode and the gate. A small current pulse at the gate opens the connection from anode to cathode. Unlike the transistor, the thyristor remains conductive as long as the current does not fall below a certain threshold. The MCR100-6 I used has a typical threshold of 0.5 milliamperes. Since the power consumption in sleep mode is much lower, the thyristor locks. The thyristor is “ignited” when the tare button is pressed. This is what the circuit looks like:

Circuit for the HX711 balance with automatic shutdown
Circuit for the balance with automatic shutdown

But there was a problem: As long as the thyristor is open, the voltage at gate remains high. Since we have a connection to the tare function, the voltage at the interrupt pin remains also high (orange line). This means that the tare request no longer works. That’s why I’ve installed a diode that blocks the way in that direction.

You can use the previous sketch. Lines 122 to 125, i.e. the actions after the wake-up, are of course never executed and can be deleted accordingly. 

When designing the power supply, it should be noted that about 0.8 volts fall off the thyristor. Accordingly, less voltage is available to the components. With 5 volt power supply, this is no problem for the components used here. 

Acknowledgement

I would like to thank Olav Kallhovd for his great library. The libraries of Adafruit helped me control the display.

I owe the scales on the post picture to Gerhard Gellinger on Pixabay. I have used the Arduino in the background several times. It comes from Seven_au on Pixabay.

Leave a Reply

Your email address will not be published. Required fields are marked *