This project automates the process of bagging soil by weight using a load cell scale and servo-controlled gate. The system dispenses soil into bags to a precise target weight, displaying real-time measurements on an LCD screen while the Arduino manages the filling cycle from start to finish.
Builders will assemble a complete weighing and dispensing station using an HX711 load cell amplifier, a continuous rotation servo for gate control, and an LCD display for feedback. The guide includes a wiring diagram, complete parts list, calibration procedures, and step-by-step assembly instructions to get the system running supervised batches of accurately portioned soil.
Wiring diagram

Gather all the parts
Assemble it in 6 steps
1. Build a tiny supervised gate
Use a small hopper, thin freely sliding gate and short-travel rack-and-pinion linkage for the SG90-360 continuous rotation servo. Keep the hopper frame separate from the weighing platform. No limit switches are used. Provide a manual way to block soil flow. Begin with an empty hopper, then very small charges of dry sifted loam, never more than 200 g for initial trials. The target bag weight is 100 g.
- Timed movement cannot detect position or a jam. This is only a supervised demonstration, not a reliable automatic packaging machine. Guard pinch points. Power loss may leave the gate open. Never drive against hard stops.
2. Connect the scale
Fix one end of the 5 kg cell to the base and the other to the platform with clearance to flex. The platform must not touch the hopper. Connect E+, E-, A+, A- to matching amplifier terminals. On the SparkFun breakout these are RED, BLK, GRN, WHT respectively; follow the cell maker labels rather than assumed wire colours. Leave YLW unused. Connect amplifier VCC and VDD to Uno 5 V, GND to ground, DAT to A0 and CLK to A1.
3. Wire the bare LCD
Connect LCD RS to D2, EN to D3, and D4–D7 to Uno D4–D7. Connect RW and GND to ground, VCC to 5 V. Leave LCD D0–D3 unused. Connect potentiometer outer terminals to 5 V and ground, centre to VO. Feed LED+ through the 220 ohm resistor from 5 V, and LED- to ground.
4. Connect rockers and servo power
Connect START T1 to D8 and STOP T1 to D10, with both T2 terminals to ground. Use non-illuminated two-terminal SPST rockers. Servo signal goes to D9. Power servo from the separate regulated 5 V 3 A supply and join its ground to Uno ground. Connect capacitor plus to servo 5 V and minus to ground. Keep servo power off the breadboard. Power Uno by USB. START must go OFF then ON for each action. STOP ON requests a timed closing movement during a cycle; keep it ON until motion ends, then turn OFF.
- Do not connect the external positive supply to Uno 5 V. Observe capacitor polarity. STOP is a software closure request, not a safety-rated emergency stop or a guarantee of closure.
5. Tune travel before adding soil
With power disconnected manually place the gate closed without forcing the servo gears; uncouple the linkage if necessary. With the linkage uncoupled verify NEUTRAL_US stops the servo and OPEN_US and CLOSE_US turn in opposite correct directions. Tune neutral if it creeps. Reconnect only with power off. OPEN_MS and CLOSE_MS are initially 300 milliseconds, not measured travel times. Adjust these constants and Deploy again until the gate opens and closes without hitting hard stops. Check closure after every test. Opening may be interrupted early by weight or STOP, so timed closing can overrun; the mechanism must tolerate this without driving into a hard stop. Do not use a rigid end-stop mechanism with this switchless design.
6. Calibrate and run supervised batches
With the gate manually verified closed and platform empty, open the serial monitor at 115200 baud. Send z, add a known 1000 g mass, then send k to save calibration. Remove the mass. After reboot the first START OFF-to-ON action acknowledges that you manually checked closure and arms the system; it does not home the gate. Place an empty bag and flip START OFF then ON again to tare and dispense. Closure starts at 90 g; tune CUTOFF_G for falling soil and check final settled weight. Before each next batch visually check full closure and fit an empty bag. On an aborted cycle the gate is not guaranteed closed. Block the hopper manually, remove soil, inspect and reset before retrying.
- Never add soil until empty-hopper travel tests pass. Timings vary with load, supply voltage and friction. If consistent automatic closure is needed, use position feedback or a suitable positional actuator instead.
Review all connections
1. Connections between "scale" and "Arduino"
2. Connections between "cell" and "Arduino"
3. Connections between "lcd" and "Arduino"
4. Connections between "contrast" and "Arduino"
5. Connections between "backlight" and "Arduino"
6. Connections between "servo" and "Arduino"
7. Connections between "supply" and "Arduino"
8. Connections between "bulk" and "Arduino"
9. Connections between "start" and "Arduino"
10. Connections between "stop" and "Arduino"
Deploy the firmware
#include <Arduino.h>
#include <HX711.h>
#include <LiquidCrystal.h>
#include <Servo.h>
#include <EEPROM.h>
#include <math.h>
LiquidCrystal lcd(2,3,4,5,6,7);
HX711 scale;
Servo gate;
const int START=8, STOP=10;
const int NEUTRAL_US=1500, OPEN_US=1700, CLOSE_US=1300;
// Trial timings only: measure and tune with an EMPTY hopper. Never run against hard stops.
const unsigned long OPEN_MS=300, CLOSE_MS=300;
const float TARGET_G=100, CUTOFF_G=90; // tune for soil still falling during closure
struct Calibration {uint32_t magic; float factor;} cal;
enum State {LOCKED, READY, TARING, OPENING, FILLING, CLOSING, DONE, FAULT};
State state=LOCKED;
unsigned long entered=0,lastSample=0;
float raw=0,offset=0,grams=0,sum=0; uint8_t samples=0;
bool haveSample=false,previousStart=false,aborted=false;
char oldLine[2][17]={{0},{0}};
void line(uint8_t row,const char* s){char buf[17]; memset(buf,' ',16);buf[16]=0;memcpy(buf,s,min((size_t)16,strlen(s)));if(strcmp(buf,oldLine[row])){lcd.setCursor(0,row);lcd.print(buf);strcpy(oldLine[row],buf);}}
bool hit(int pin){return digitalRead(pin)==LOW;}
void setState(State s){state=s;entered=millis();}
void closeGate(bool abortCycle){aborted=abortCycle;setState(CLOSING);gate.writeMicroseconds(CLOSE_US);}
void fault(){gate.writeMicroseconds(NEUTRAL_US);setState(FAULT);}
void setup(){
pinMode(START,INPUT_PULLUP);pinMode(STOP,INPUT_PULLUP);
gate.attach(9);gate.writeMicroseconds(NEUTRAL_US);lcd.begin(16,2);Serial.begin(115200);
scale.begin(A0,A1,false,false);EEPROM.get(0,cal);
if(cal.magic!=0x534F494CUL || !isfinite(cal.factor) || fabs(cal.factor)<0.001){cal.magic=0;cal.factor=0;}
Serial.println(F("Manually CLOSE gate before power-on. First START acknowledges closed gate; next START runs. Timed gate has NO position feedback. Calibration with gate CLOSED: empty platform send z; add known 1000 g mass send k; remove mass. Flip START OFF then ON to tare empty bag. STOP ON aborts. Reboot to clear a fault."));
}
void loop(){
unsigned long now=millis();bool fresh=false;
if(scale.is_ready()){raw=scale.read();lastSample=now;haveSample=true;fresh=true;if(cal.factor)grams=(raw-offset)/cal.factor;}
bool pressed=hit(START); bool edge=pressed&&!previousStart;previousStart=pressed;
if(Serial.available()){
char c=Serial.read();
if((state==LOCKED||state==READY||state==DONE)&&haveSample&&now-lastSample<500){
if(c=='z'){offset=raw;Serial.println(F("Zero captured. Place known 1000 g mass, then send k."));}
if(c=='k'){float f=(raw-offset)/1000.0f;if(fabs(f)>0.001){cal.magic=0x534F494CUL;cal.factor=f;EEPROM.put(0,cal);Serial.print(F("Saved counts/gram: "));Serial.println(f,5);}}
}
}
if(hit(STOP)&&(state==TARING||state==OPENING||state==FILLING))closeGate(true);
if((state==TARING||state==OPENING||state==FILLING)&&now-lastSample>1000)closeGate(true);
switch(state){
case LOCKED:
if(edge&&!hit(STOP)&&cal.magic){setState(READY);}break;
case READY:case DONE:
if(edge&&!hit(STOP)&&cal.magic&&haveSample&&now-lastSample<500) {sum=0;samples=0;setState(TARING);}break;
case TARING:
if(fresh){sum+=raw;if(++samples>=10){offset=sum/samples;grams=0;gate.writeMicroseconds(OPEN_US);setState(OPENING);}}break;
case OPENING:
if(fresh&&grams>=CUTOFF_G)closeGate(false);
else if(now-entered>=OPEN_MS){gate.writeMicroseconds(NEUTRAL_US);setState(FILLING);}
else if(now-entered>5000)closeGate(true);break;
case FILLING:
if(fresh&&(grams>=CUTOFF_G||grams < -20))closeGate(grams< -20);
else if(now-entered>60000)closeGate(true);break;
case CLOSING:
if(now-entered>=CLOSE_MS){gate.writeMicroseconds(NEUTRAL_US);setState(aborted?LOCKED:DONE);if(aborted&&cal.magic&&!hit(STOP))setState(READY);}
else if(now-entered>5000)fault();break;
case FAULT:break;
}
const char* names[]={"Check gate shut","Bag + START","Taring bag...","Opening gate","Filling soil","Closing gate","Check shut / bag","FAULT: manual"};
line(0,names[state]); char text[17];
if(!cal.magic)strcpy(text,"Serial: z then k");else if(!haveSample||now-lastSample>1000)strcpy(text,"Check HX711");else{snprintf(text,sizeof(text),"%ld g / 100 g",(long)grams);}line(1,text);
}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.




