Community project
Filtered Digital Weighing Scale
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

Gather all the parts
| Qty | Component |
|---|---|
| 1 | 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 | 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 | Optional TARE button Momentary push button switch |
| 1 | Optional CAL button Momentary push button switch |
| 1 | 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"
| Function | hx711_1 | Arduino |
|---|---|---|
| digital | DT | GPIO 6 |
| digital | SCK | GPIO 2 |
| power | E+ → Load Cell E+ | EXT |
| ground | E- → Load Cell E- | EXT |
| analog | A+ → Load Cell A+ | EXT |
| analog | A- → Load Cell A- | EXT |
| ground | GND | GND |
2. Connections between "tare_button" and "Arduino"
| Function | tare_button | Arduino |
|---|---|---|
| ground | GND | GND |
| digital | SIGNAL | GPIO 4 |
3. Connections between "cal_button" and "Arduino"
| Function | cal_button | Arduino |
|---|---|---|
| ground | GND | GND |
| digital | SIGNAL | GPIO 5 |
4. Connections between "supply_12v_1" and "Arduino"
| Function | supply_12v_1 | Arduino |
|---|---|---|
| power | +12V → Adjustable 12 V to 5 V DC-DC Buck Converter Module IN+ | EXT |
| ground | GND → Adjustable 12 V to 5 V DC-DC Buck Converter Module IN- | EXT |
5. Connections between "buck_12v_to_5v_1" and "Arduino"
| Function | buck_12v_to_5v_1 | Arduino |
|---|---|---|
| power | OUT+ → HX711 Load Cell Amplifier VCC | EXT |
| ground | OUT- → HX711 Load Cell Amplifier GND | EXT |
Deploy the firmware
#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.




