Community project

Filtered Digital Weighing Scale

Arduino
Photo of Filtered Digital Weighing Scale
Generated with AI

Ragul Murugan

Published September 1, 2026

This project builds a precision digital weighing scale around the HX711 load cell amplifier and Arduino Uno microcontroller. The scale reads weight measurements through a serial interface, offering tare and calibration functions controlled via push buttons. Builders will receive a complete wiring diagram, parts list, step-by-step assembly instructions, and Arduino firmware with built-in calibration wizard and data filtering.

The design uses a 12V power supply stepped down to 5V via an adjustable buck converter, ensuring stable operation of the load cell and microcontroller. No display is required—all interaction happens through USB Serial commands, making this an ideal platform for integrating weighing capability into larger projects or data logging systems.

Wiring diagram

Wiring diagram for Filtered Digital Weighing Scale

Gather all the parts

QtyComponent
1

HX711 Load Cell Amplifier

5 V HX711 module

24-bit ADC front-end for strain-gauge load cells and weigh scales. DT/DOUT and SCK are MCU-facing pins; E+/E-/A+/A- connect to the load cell bridge.

1

Load Cell

4 × 50 kg, 3-wire half-bridge Wheatstone bridge

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

1

Push Button

Optional TARE button

Momentary push button switch

1

Push Button

Optional CAL button

Momentary push button switch

1

12V Barrel-Jack Adapter

12 V, 2 A

12 V / 2 A wall adapter with a 5.5 mm / 2.1 mm barrel jack. Used to power motor drivers, LED strips, or boards that need a higher rail.

1

Adjustable 12 V to 5 V DC-DC Buck Converter Module

Adjustable, set to 5.0 V

An adjustable step-down converter module that changes the 12-volt adapter supply into regulated 5 volts for the HX711.

Assemble it in 8 steps

1. Turn all power off

Unplug the Nano USB cable and unplug the 12 V wall adapter before changing any wire. Keep the load-cell platform still so its thin wires are not pulled.

  • Keep the HX711 close to the load-cell wire junctions and away from the 12 V leads.
  • Use strain relief so the platform cannot tug on the load-cell wires.
  • Do not move wires while either supply is powered; a slipped wire can damage a module or cause unreliable readings.

2. Leave the load-cell wires on the HX711

Keep the four load-cell bridge junctions connected only to HX711 E+, E−, A+, and A−. Do not connect these thin sensor wires to the Nano, the 12 V adapter, or the converter.

  • Use soldered insulated joins or a protected terminal block.
  • Keep the load-cell wires together and away from the 12 V leads.
  • Do not connect any load-cell bridge junction to 5 V or GND; that prevents correct measurement.

3. Connect 12 V to the converter input

Connect the 12 V adapter positive output to converter IN+ (power). Connect the 12 V adapter negative output to converter IN− (ground).

  • Match the + and − markings printed on the converter.
  • Use insulated terminals so the 12 V wires cannot touch each other.
  • Reversing IN+ and IN− can damage the converter. Never connect the 12 V adapter directly to the HX711 or Nano 5 V pin.

4. Set the converter to exactly 5.0 V

Before connecting the converter output to the HX711, plug in the 12 V adapter and use a multimeter across OUT+ and OUT−. Turn the small adjustment screw until the meter reads 5.0 V, then unplug the 12 V adapter again.

  • Turn the screw in very small movements and recheck the meter after each movement.
  • Mark the adjusted screw position after setting it.
  • Do not connect OUT+ to the HX711 until it measures 5.0 V. A higher output can damage the HX711.

5. Power the HX711 from the converter

Connect converter OUT+ to HX711 VCC (regulated 5 V power). Connect converter OUT− to HX711 GND (ground). Do not connect the Nano 5 V pin to HX711 VCC.

  • Keep the 5 V output wires short and firmly clamped.
  • The HX711 should receive VCC from this converter only.
  • Make sure OUT+ and OUT− are not swapped — swapped power can damage the HX711.

6. Connect the Nano to the HX711

Connect Nano GND to HX711 GND (shared ground). Connect HX711 DT/DOUT to Nano D6 (data). Connect HX711 SCK/CLK to Nano D2 (clock).

  • HX711 GND has two ground connections: converter OUT− and Nano GND.
  • The shared ground lets the Nano interpret the HX711 data signal correctly.
  • Do not join converter OUT+ to the Nano USB 5 V line; share ground only.

7. Remove the OLED completely

Leave the OLED disconnected: remove its VCC, GND, SDA, and SCL wires from the Nano. The scale now reports readings, calibration prompts, and status through the USB Serial connection only.

  • Nano A4 and A5 are no longer used by this project.
  • Keep the removed display and its wires aside so they cannot short a Nano pin.
  • Do not leave loose display wires touching the Nano; they can cause intermittent readings or resets.

8. Power up and use Serial commands

Plug in the Nano USB cable first, then plug in the 12 V adapter. Open the Serial connection at 115200 baud. With the platform empty and untouched, send RAW several times. When readings settle, run CAL 10, then ZERO, place the 10 kg reference in the center, and send CAL.

  • Use STATUS to see whether the scale considers the reading stable.
  • If RAW still jumps by tens of thousands of counts while untouched, inspect the load-cell joints and platform mechanics.
  • Do not trust a calibration until the empty and loaded raw readings remain reasonably steady.

Review all connections

1. Connections between "hx711_1" and "Arduino"

Functionhx711_1Arduino
digitalDTGPIO 6
digitalSCKGPIO 2
powerE+Load Cell E+EXT
groundE-Load Cell E-EXT
analogA+Load Cell A+EXT
analogA-Load Cell A-EXT
groundGNDGND

2. Connections between "tare_button" and "Arduino"

Functiontare_buttonArduino
groundGNDGND
digitalSIGNALGPIO 4

3. Connections between "cal_button" and "Arduino"

Functioncal_buttonArduino
groundGNDGND
digitalSIGNALGPIO 5

4. Connections between "supply_12v_1" and "Arduino"

Functionsupply_12v_1Arduino
power+12VAdjustable 12 V to 5 V DC-DC Buck Converter Module IN+EXT
groundGNDAdjustable 12 V to 5 V DC-DC Buck Converter Module IN-EXT

5. Connections between "buck_12v_to_5v_1" and "Arduino"

Functionbuck_12v_to_5v_1Arduino
powerOUT+HX711 Load Cell Amplifier VCCEXT
groundOUT-HX711 Load Cell Amplifier GNDEXT

Deploy the firmware

DigitalScaleV2.inoOpen in Schematik
#include <Arduino.h>
#include <EEPROM.h>
#include <HX711.h>
#include <avr/wdt.h>
#include <math.h>
#include <string.h>

#define CLK_PIN 2
#define TARE_PIN 4
#define CAL_PIN 5

// hPod Digital Scale V2.7 — Arduino Nano / ATmega328P
// USB Serial is the user interface; no display is fitted.

enum ScaleStatus : byte { READY, MEASURING, STABLE, CALIBRATING, OVERLOAD, HX_ERROR, CAL_REQUIRED };
enum CalState : byte { CAL_IDLE, CAL_REMOVE, CAL_ZERO, CAL_PLACE, CAL_LOAD, CAL_SUCCESS };
struct Store { uint32_t magic; uint16_t version; long zero; float cal; uint16_t crc; };


// Forward declarations
uint16_t crc16(const byte *p, size_t n);
void sort7(long *a);
void resetFilters();
bool loadEEPROM();
bool saveEEPROM();
bool readHX711();
void processWeight();
void chooseStatus();
const __FlashStringHelper* statusText();
void startCapture(bool calibrationZero);
void tare();
void calibrationWizard();
void completeCapture();
void printStatus();
bool parseMass(char *s,float &v);
void executeCommand(char *c);
void serialCommandHandler();
void serviceButtons();
void serialDebug();

const byte DOUT_PIN=6, CLK_PIN=2, TARE_PIN=4, CAL_PIN=5;
const long DEFAULT_ZERO=704825L;
const float DEFAULT_CAL=20225.2f, ALPHA=0.10f, MAX_KG=200.0f;
const unsigned long SAMPLE_MS=80UL, TIMEOUT_MS=1000UL, STABLE_MS=1000UL, AUTOZERO_MS=1000UL;
const uint32_t MAGIC=0x48504F44UL;
const uint16_t FORMAT=0x0205;

HX711 hx;
bool hxOK=false, eepromOK=false, haveWeight=false, stableOK=false;
long rawADC=0, filteredADC=0, zeroOffset=DEFAULT_ZERO, captureSum=0, calibrationZero=0;
long captureMin=0, captureMax=0;
float calFactor=DEFAULT_CAL, referenceKg=10.0f, weightKg=0, lastAccepted=0, spikeCandidate=0;
long median[7];
float average[10], averageSum=0, stableLow=0, stableHigh=0;
byte medianN=0, medianI=0, averageN=0, averageI=0, spikeN=0, captureN=0;
bool filterReady=false, captureIsCalibrationZero=false, tareArmed=true, calDone=false;
unsigned long lastSample=0, lastGood=0, stableSince=0, lastAutoZero=0, lastDebug=0, successAt=0;
char command[24]; byte commandN=0; unsigned long lastChar=0;
unsigned long tareDownAt=0, calDownAt=0;
ScaleStatus status=READY;
CalState calState=CAL_IDLE;

uint16_t crc16(const byte *p, size_t n) { uint16_t c=0xFFFF; while(n--) { c^=*p++; for(byte i=0;i<8;i++) c=(c&1)?(c>>1)^0xA001:c>>1; } return c; }
void sort7(long *a) { for(byte i=1;i<7;i++) { long k=a[i]; int j=i-1; while(j>=0 && a[j]>k) { a[j+1]=a[j]; j--; } a[j+1]=k; } }
void resetFilters() { medianN=medianI=averageN=averageI=spikeN=0; averageSum=0; haveWeight=false; filterReady=false; stableOK=false; stableSince=0; }

bool loadEEPROM() { Store s; EEPROM.get(0,s); if(s.magic!=MAGIC || s.version!=FORMAT || s.crc!=crc16((byte*)&s,sizeof(s)-2) || !isfinite(s.cal) || s.cal<1000 || s.cal>1000000) { zeroOffset=DEFAULT_ZERO; calFactor=DEFAULT_CAL; eepromOK=false; return false; } zeroOffset=s.zero; calFactor=s.cal; eepromOK=true; return true; }
bool saveEEPROM() { Store s={MAGIC,FORMAT,zeroOffset,calFactor,0}; s.crc=crc16((byte*)&s,sizeof(s)-2); EEPROM.put(0,s); Store verify; EEPROM.get(0,verify); eepromOK=!memcmp(&s,&verify,sizeof(s)); return eepromOK; }

bool readHX711() { unsigned long now=millis(); if(now-lastSample<SAMPLE_MS) return false; lastSample=now; if(!hx.is_ready()) { hxOK=false; return false; } rawADC=hx.read(); hxOK=true; lastGood=now; return true; }
void processWeight() {
  median[medianI]=rawADC; medianI=(medianI+1)%7; if(medianN<7) { medianN++; return; }
  long ordered[7]; for(byte i=0;i<7;i++) ordered[i]=median[i]; sort7(ordered); filteredADC=ordered[3]; filterReady=true;
  float candidate=(filteredADC-zeroOffset)/calFactor;
  if(!haveWeight) lastAccepted=candidate;
  else if(fabs(candidate-lastAccepted)>0.50f) { if(!spikeN || fabs(candidate-spikeCandidate)>0.10f) { spikeCandidate=candidate; spikeN=1; } else spikeN++; if(spikeN>=5) { lastAccepted=spikeCandidate; spikeN=0; } }
  else { lastAccepted=candidate; spikeN=0; }
  if(averageN<10) { average[averageI]=lastAccepted; averageSum+=lastAccepted; averageN++; } else { averageSum-=average[averageI]; average[averageI]=lastAccepted; averageSum+=lastAccepted; } averageI=(averageI+1)%10;
  float mean=averageSum/averageN; if(!haveWeight) weightKg=mean; else weightKg+=ALPHA*(mean-weightKg); haveWeight=true;
  if(!stableSince) { stableLow=stableHigh=weightKg; stableSince=millis(); stableOK=false; }
  else { if(weightKg<stableLow) stableLow=weightKg; if(weightKg>stableHigh) stableHigh=weightKg; if(stableHigh-stableLow>=0.040f) { stableLow=stableHigh=weightKg; stableSince=millis(); stableOK=false; } else if(millis()-stableSince>=STABLE_MS) stableOK=true; }
  if(calState==CAL_IDLE && stableOK && fabs(weightKg)<0.050f && millis()-lastAutoZero>=AUTOZERO_MS) { zeroOffset+=(long)((rawADC-zeroOffset)*0.002f); lastAutoZero=millis(); }
}
void chooseStatus() { if(!hxOK && millis()-lastGood>TIMEOUT_MS) status=HX_ERROR; else if(calState!=CAL_IDLE) status=CALIBRATING; else if(!eepromOK) status=CAL_REQUIRED; else if(haveWeight && weightKg>MAX_KG) status=OVERLOAD; else if(stableOK) status=STABLE; else status=haveWeight?MEASURING:READY; }
const __FlashStringHelper* statusText() { switch(status) { case READY:return F("READY"); case MEASURING:return F("MEASURING"); case STABLE:return F("STABLE"); case CALIBRATING:return F("CALIBRATING"); case OVERLOAD:return F("OVERLOAD"); case HX_ERROR:return F("HX711 ERROR"); default:return F("CAL REQUIRED"); } }

void startCapture(bool calibrationZero) { if(!hxOK) { Serial.println(F("ERR=HX711_NOT_READY")); return; } captureSum=0; captureN=0; captureMin=0; captureMax=0; captureIsCalibrationZero=calibrationZero; calState=CAL_ZERO; resetFilters(); Serial.println(calibrationZero?F("ZERO=CAPTURING_STABLE_SAMPLES"):F("TARE=CAPTURING_STABLE_SAMPLES")); }
void tare() { startCapture(false); }
void calibrationWizard() { if(calState==CAL_IDLE) { calState=CAL_REMOVE; Serial.print(F("CAL=REMOVE_WEIGHT; reference=")); Serial.println(referenceKg,3); } else if(calState==CAL_PLACE) { captureSum=0; captureN=0; captureMin=0; captureMax=0; resetFilters(); calState=CAL_LOAD; Serial.println(F("CAL=CAPTURING_FILTERED_REFERENCE")); } else Serial.println(F("ERR=CALIBRATION_BUSY")); }
void completeCapture() {
  if((calState!=CAL_ZERO && calState!=CAL_LOAD) || captureN<30) return;
  long allowedSpan=(long)(calFactor*0.040f); if(allowedSpan<1) allowedSpan=1;
  if(captureMax-captureMin>allowedSpan) { Serial.print(F("ERR=CAPTURE_UNSTABLE SPAN=")); Serial.print(captureMax-captureMin); Serial.print(F(" LIMIT=")); Serial.println(allowedSpan); resetFilters(); calState=CAL_IDLE; return; }
  long v=captureSum/captureN;
  if(calState==CAL_ZERO) { if(captureIsCalibrationZero) { calibrationZero=v; resetFilters(); calState=CAL_PLACE; Serial.println(F("ZERO=CAPTURED; PLACE REFERENCE; SEND CAL")); } else { zeroOffset=v; resetFilters(); calState=CAL_IDLE; Serial.print(F("TARE=OK ZERO=")); Serial.println(zeroOffset); } }
  else { float f=(v-calibrationZero)/referenceKg; if(!isfinite(f) || f<1000 || f>1000000) { calState=CAL_IDLE; Serial.println(F("ERR=INVALID_REFERENCE_READING")); return; } zeroOffset=calibrationZero; calFactor=f; saveEEPROM(); resetFilters(); calState=CAL_SUCCESS; successAt=millis(); Serial.print(F("CAL=SUCCESS FACTOR=")); Serial.println(calFactor,2); }
}
void printStatus() { Serial.print(F("HX711=")); Serial.println(hxOK?F("OK"):F("ERROR")); Serial.print(F("EEPROM=")); Serial.println(eepromOK?F("OK"):F("INVALID")); Serial.print(F("STABLE=")); Serial.println(stableOK?F("YES"):F("NO")); Serial.print(F("STATE=")); Serial.println(statusText()); Serial.print(F("WT=")); Serial.println(weightKg,3); Serial.print(F("RAW=")); Serial.println(rawADC); Serial.print(F("ZERO=")); Serial.println(zeroOffset); Serial.print(F("CAL=")); Serial.println(calFactor,2); }
bool parseMass(char *s,float &v) { bool digit=false, point=false; float whole=0, frac=0, divisor=1; while(*s==' ') s++; while(*s) { if(*s>='0'&&*s<='9') { digit=true; if(point) { divisor*=10; frac+=(*s-'0')/divisor; } else whole=whole*10+*s-'0'; } else if(*s=='.'&&!point) point=true; else if(*s!=' ') return false; s++; } v=whole+frac; return digit && v>0 && v<=MAX_KG; }
void executeCommand(char *c) { for(byte i=0;c[i];i++) if(c[i]>='a'&&c[i]<='z') c[i]-=32; char *arg=c; while(*arg && *arg!=' ') arg++; if(*arg) { *arg++=0; while(*arg==' ') arg++; } if(!strcmp(c,"HELP")) Serial.println(F("HELP READ RAW STATUS VERSION TARE CAL [kg] ZERO SAVE LOAD RESET")); else if(!strcmp(c,"READ")) { Serial.print(F("WT=")); Serial.println(weightKg,3); } else if(!strcmp(c,"RAW")) { Serial.print(F("RAW=")); Serial.println(rawADC); } else if(!strcmp(c,"STATUS")) printStatus(); else if(!strcmp(c,"VERSION")) Serial.println(F("Digital Scale Firmware V2.7 SERIAL-ONLY")); else if(!strcmp(c,"TARE")) tare(); else if(!strcmp(c,"CAL")) { if(*arg && !parseMass(arg,referenceKg)) Serial.println(F("ERR=CAL_MASS_RANGE")); else calibrationWizard(); } else if(!strcmp(c,"ZERO")) { if(calState==CAL_REMOVE) startCapture(true); else tare(); } else if(!strcmp(c,"SAVE")) Serial.println(saveEEPROM()?F("SAVE=OK"):F("SAVE=ERROR")); else if(!strcmp(c,"LOAD")) { loadEEPROM(); resetFilters(); Serial.println(eepromOK?F("LOAD=OK"):F("LOAD=INVALID")); } else if(!strcmp(c,"RESET")) { Serial.println(F("RESET=NOW")); Serial.flush(); wdt_enable(WDTO_15MS); } else Serial.println(F("ERR=UNKNOWN_COMMAND")); }
void serialCommandHandler() { while(Serial.available()) { char c=Serial.read(); if(c=='\r'||c=='\n') { if(commandN) { command[commandN]=0; executeCommand(command); commandN=0; } } else if(commandN<sizeof(command)-1) { command[commandN++]=c; lastChar=millis(); } } if(commandN && millis()-lastChar>120) { command[commandN]=0; executeCommand(command); commandN=0; } }
void serviceButtons() { bool t=digitalRead(TARE_PIN)==LOW; if(t && tareArmed) { if(!tareDownAt) tareDownAt=millis(); if(millis()-tareDownAt>=30) { tareArmed=false; tare(); } } if(!t) { tareArmed=true; tareDownAt=0; } bool c=digitalRead(CAL_PIN)==LOW; if(c && !calDownAt) { calDownAt=millis(); calDone=false; } if(c && !calDone && millis()-calDownAt>=2000) { calDone=true; calibrationWizard(); } if(!c) { calDownAt=0; calDone=false; } }
void serialDebug() { if(millis()-lastDebug<5000) return; lastDebug=millis(); Serial.print(F("DBG raw=")); Serial.print(rawADC); Serial.print(F(" kg=")); Serial.print(weightKg,3); Serial.print(F(" status=")); Serial.println(statusText()); }
void setup() { Serial.begin(115200); pinMode(TARE_PIN,INPUT_PULLUP); pinMode(CAL_PIN,INPUT_PULLUP); hx.begin(DOUT_PIN,CLK_PIN); hx.set_gain(128); loadEEPROM(); lastGood=millis(); Serial.println(F("Digital Scale Firmware V2.7 SERIAL-ONLY")); }
void loop() { serialCommandHandler(); serviceButtons(); if(readHX711()) { processWeight(); if((calState==CAL_ZERO || calState==CAL_LOAD) && filterReady) { if(captureN==0) captureMin=captureMax=filteredADC; else { if(filteredADC<captureMin) captureMin=filteredADC; if(filteredADC>captureMax) captureMax=filteredADC; } captureSum+=filteredADC; captureN++; } } completeCapture(); if(calState==CAL_SUCCESS && millis()-successAt>2500) calState=CAL_IDLE; chooseStatus(); serialDebug(); }

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik