MicroPython Code#
Let’s first convert the analog output from the load cell to a digital representation.
The figure below shows the relationship between analog and digital values of the ESP32 for several ADC gain settings. Since our signal is centered around \(V_\textrm{ref}\approx 1.65V\) we use the 11dB attenuation setting.
You can also try with less attenuation, possibly lowering \(V_\textrm{ref}\). Check the minimum input voltage in the INA126 datasheet.
At the beginning of each notebook, we need to set up the path and connect to the microcontroller:
%connect balance
Connected to balance @ serial:///dev/ttyUSB0
Now we are ready to setup and read the ADC. The code below is adapted from the example in the MicroPython documentation.
from machine import ADC, Pin
import time
# configure ADC3 (output of the INA126)
out = ADC(Pin(39))
out.atten(ADC.ATTN_11DB)
# configure ADC6 (Vref)
ref = ADC(Pin(34))
ref.atten(ADC.ATTN_11DB)
# read the ADCs in a loop and display the result
# _ just means that we don't care for the loop counter
for _ in range(10):
vout = out.read()
vref = ref.read()
print("out = {:4} ref = {:4} delta = {:4}".format(vout, vref, vout-vref))
time.sleep(1)
out = 1810 ref = 1717 delta = 93
out = 1787 ref = 1723 delta = 64
out = 2153 ref = 1712 delta = 441
out = 2084 ref = 1736 delta = 348
out = 2114 ref = 1726 delta = 388
out = 2101 ref = 1703 delta = 398
out = 2113 ref = 1729 delta = 384
out = 1772 ref = 1695 delta = 77
out = 1823 ref = 1714 delta = 109
out = 1773 ref = 1725 delta = 48
Playing around with the scale, you can see that the output (delta
) changes when you put a weight (e.g. your finger) on the scale. But even with no weight applied, the delta
is not zero. This error is called “offset” and comes from inacurracies in the load cell, the INA216, and the ADC.
Further, values reported by the ADC change even for constant weight. This “noise” is the result of electrical interference and the ADC.
Let’s try averaging a few samples to see if we can reduce the noise:
for N in [1, 10, 100]:
for _ in range(5):
vout = 0
vref = 0
for _ in range(N):
vout += out.read()
vref += ref.read()
vout /= N
vref /= N
print("N = {:3} out = {:4.0f} ref = {:4.0f} delta = {:4.0f}".format(
N, vout, vref, vout-vref-55))
time.sleep(1)
print()
N = 1 out = 1780 ref = 1718 delta = 7
N = 1 out = 1726 ref = 1725 delta = -54
N = 1 out = 1775 ref = 1719 delta = 1
N = 1 out = 1777 ref = 1734 delta = -12
N = 1 out = 1759 ref = 1726 delta = -22
N = 10 out = 1780 ref = 1717 delta = 8
N = 10 out = 1779 ref = 1723 delta = 0
N = 10 out = 1776 ref = 1721 delta = 0
N = 10 out = 1777 ref = 1724 delta = -2
N = 10 out = 1777 ref = 1723 delta = -0
N = 100 out = 1776 ref = 1722 delta = -1
N = 100 out = 1776 ref = 1722 delta = -0
N = 100 out = 1774 ref = 1722 delta = -3
N = 100 out = 1777 ref = 1721 delta = 0
N = 100 out = 1777 ref = 1722 delta = 0
In these tests I did not apply any force to the scale.
Averaging definitely helps. In my trials it reduced the noise (variations of delta
) from more than 50 without averaging (N=1) to less than 5 (N=100), a ten-fold improvement!
Let’s update the code again, this time first measuring the offset and then subtracting it from subsequent measurements. We also create a function for reading the ADC and averaging its outputs. In read_adc
we average the difference, a small optimization to keep the sum smaller, even for large N.
def read_adc(out, ref, N=100):
sum = 0
for _ in range(N):
sum += out.read() - ref.read()
return sum/N
# measure the offset
offset = read_adc(out, ref)
# weigh ...
for _ in range(10):
weight = read_adc(out, ref) - offset
print("weight = {:4.0f}".format(weight))
time.sleep(1)
weight = -4
weight = -0
weight = 0
weight = 332
weight = 334
weight = 332
weight = 0
weight = -2
weight = -1
weight = -3
Not perfect but somewhat usable.
Let’s now calibrate the scale so it’s output is in grams. For this we need a reference with known weight. If you do not have calibrated weights just get something with a weight close to the full scale of your load cell, get another scale to determine its weight, and then put it on your scale.
offset = read_adc(out, ref)
for _ in range(5):
print("weight = {:4.0f}".format(read_adc(out, ref) - offset))
time.sleep(2)
weight = 3
weight = 839
weight = 831
weight = 839
weight = 839
My reference weighs 500grams. The output of the scale is about 840 (averaged), so let’s redo the test with the output scaled by 500/840.
offset = read_adc(out, ref)
for _ in range(5):
weight = read_adc(out, ref) - offset
weight_scaled = weight * 500 / 840
print("weight = {:4.0f} grams".format(weight_scaled))
time.sleep(2)
weight = -0 grams
weight = 0 grams
weight = -501 grams
weight = -500 grams
weight = -501 grams
Ups, I forgot to remove the weight before I started the test. Now it comes out negative: I removed 500grams.
As a final step, let’s wrap up the code in a class.
from machine import Pin, ADC
import time
class Scale:
def __init__(self, out_pin=39, ref_pin=34, scale=500/840):
self._out = ADC(Pin(out_pin))
self._out.atten(ADC.ATTN_11DB)
self._ref = ADC(Pin(ref_pin))
self._ref.atten(ADC.ATTN_11DB)
self._scale = scale
self._offset = self._read_adc()
def _read_adc(self, N=100):
sum = 0
out = self._out
ref = self._ref
for _ in range(N):
sum += out.read() - ref.read()
return sum/N
def measure(self):
return (self._read_adc()-self._offset) * self._scale
def tare(self, button):
print("tare")
self._offset = self._read_adc()
scale = Scale()
last_weight = 1000
start = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start) < 5000:
weight = scale.measure()
# print only big changes
if abs(weight - last_weight) > 10:
print("{:5.0f} gram".format(weight))
last_weight = weight
3 gram
43 gram
312 gram
335 gram
456 gram
262 gram
281 gram
305 gram
294 gram
338 gram
475 gram
534 gram
498 gram
478 gram
529 gram
493 gram
520 gram
501 gram
490 gram
509 gram
491 gram
503 gram
515 gram
501 gram
487 gram
442 gram
393 gram
250 gram
145 gram
106 gram
64 gram
-9 gram
2 gram
We need a display; just printing the output isn’t user friendly.
But the Scale
class looks ok, let’s save it.
%%writefile code/lib/scale.py
from machine import Pin, ADC
import time
class Scale:
def __init__(self, out_pin=39, ref_pin=34, scale=500/840):
self._out = ADC(Pin(out_pin))
self._out.atten(ADC.ATTN_11DB)
self._ref = ADC(Pin(ref_pin))
self._ref.atten(ADC.ATTN_11DB)
self._scale = scale
self._offset = self._read_adc()
def _read_adc(self, N=100):
sum = 0
out = self._out
ref = self._ref
for _ in range(N):
sum += out.read() - ref.read()
return sum/N
def measure(self):
return (self._read_adc()-self._offset) * self._scale
def tare(self, button):
print("tare")
self._offset = self._read_adc()
Overwriting code/lib/scale.py