Digitalwaage mit HX711 und ESP8266 / ESP32 in MicroPython - Teil 1 - AZ-Delivery

This post is also available as PDF document for download.

A scale without moving parts - is they not saying? It works, says the result of my current project with a fabulous dissolution and accuracy. From, with the eye that cannot be perceptible to an aluminum cuboid, a 24-bit adc (analog digital converter), an ESP8266 or ESP32 (abbreviated in the following text ESP) and an OLED display, a digital scale that in my case Can grasp mass up to 1kg, with an accuracy of 0.01g! How this works and what tricks behind it, this post tells you from the line

Micropython on the ESP32 and ESP8266

today

Digital scale with the HX711

I originally didn't think it was possible and was absolutely surprised by the result. The aluminum squad used is a so -called load cell. Two holes in the middle thin out the metal, and stretching strips are glued on the wall, in Figure 1 at the top and bottom. The material thickness is approx. 1mm there.

Figure 1: Wage cell from the side

Figure 1: Wage cell from the side

The load cell is screwed to the right with the base plate, the strap of the scale is attached to the left.

Figure 2: Libra for 1kg

Figure 2: Libra for 1kg

A sheet of paper of the size 10cm x 15 cm now bends the load cell so much, or rather little that the scale shows the weight of 0.9g.

How does this work? Stretching strips are wafer -thin conductor tracks that are applied to a plastic carrier. These pads are glued to a carrier material.

Figure 3: Stretching strips schematic

Figure 3: Stretching strips schematic

By bending the carrier, the conductors are stretched slightly and thus thinner and longer. This causes a change in the electrical resistance, which is known to depend on the two parameters, ρ is the specific resistance, a material constant.

Figure 4: Resistance formula

Figure 4: Resistance formula

The paper on the paper is likely to trigger a bending of the 12.5mm high cuboid, which lies in the order of an atom. The resulting change of length of the measuring strip also. And that is sufficient to change the tension on the measuring bridge with the four measuring strips, two above, two below the cuboid, so far that the HX711 can derive a measurable and above all reproducible voltage change.

Figure 5: HX711 - circuit

Figure 5: HX711 - circuit

Hardware

Both ESP8266 and ESP32 models with at least four free GPIOs are suitable as a controller. The measuring value is displayed via an OLED display. Another model can of course also serve as a load cell. They are up to 100kg of weighing volume and more from 100g.

1

D1 Mini Nodemcu with ESP8266-12F WLAN module or

D1 Mini V3 Nodemcu with ESP8266-12F or

Nodemcu Lua Amica Module V2 ESP8266 ESP-12F WiFi or

Nodemcu Lua Lolin V3 Module ESP8266 ESP-12F WIFI or

ESP32 Dev Kit C unpleasant or

ESP32 Dev Kit C V4 unplacerated or

ESP32 NODEMCU Module WiFi Development Board with CP2102 or

Nodemcu-ESP-32S kit or

ESP32 Lolin Lolin32 WiFi Bluetooth Dev Kit

1

0.91 inch OLED I2C Display 128 x 32 pixels

1

Weighing cell 1kg

1

HX711 AD converter for weighing cells

1

MB-102 Breadboard Pug with 830 contacts

various

Jumper Wire cable 3 x 40 pcs. 20 cm M2M / F2M / F2F each possibly too

65stk. Jumper wire cable plug -in bridges for Breadboard

optional

Logic Analyzer

The software

For flashing and the programming of the ESP32:

Thonny or

µpycraft

Used firmware for the ESP32:

V1.19.1 (2022-06-18).

Used firmware for the ESP8266:

V1.19.1 (2022-06-18).

The micropython programs for the project:

SSD1306.PY Hardware driver for the OLED display

oled.py API for the OLED display

geometer_30.py Large character set for the digit display

hx711.py API for the AX711

scale.py The operating program

zeichensatz.rar Working environment for creating your own symbols

Micropython - Language - Modules and Programs

To install Thonny you will find one here Detailed instructions (English version). There is also a description of how that Micropython firmware (As of 18.06.2022) on the ESP chip burned becomes.

Micropython is an interpreter language. The main difference to the Arduino IDE, where you always flash entire programs, is that you only have to flash the Micropython firmware once on the ESP32 so that the controller understands micropython instructions. You can use Thonny, µpycraft or ESPTOOL.PY. For Thonny I have the process here described.

As soon as the firmware has flashed, you can easily talk to your controller in a dialogue, test individual commands and see the answer immediately without having to compile and transmit an entire program beforehand. That is exactly what bothers me on the Arduino IDE. You simply save an enormous time if you can check simple tests of the syntax and hardware to trying out and refining functions and entire program parts via the command line before knitting a program from it. For this purpose, I always like to create small test programs. As a kind of macro, they summarize recurring commands. Whole applications then develop from such program fragments.

Autostart

If the program is to start autonomously by switching on the controller, copy the program text into a newly created blank tile. Save this file under boot.py in WorkSpace and upload it to the ESP chip. The program starts automatically the next time the reset or switching on.

Test programs

Programs from the current editor window in the Thonny-IDE are started manually via the F5 button. This can be done faster than the mouse click on the start button, or via the menu run. Only the modules used in the program must be in the flash of the ESP32.

In between, Arduino id again?

Should you later use the controller together with the Arduino IDE, just flash the program in the usual way. However, the ESP32/ESP8266 then forgot that it has ever spoken Micropython. Conversely, any espressif chip that contains a compiled program from the Arduino IDE or AT-Firmware or Lua or ... can be easily provided with the micropython firmware. The process is always like here described.

The circuit

Here are the circuit diagrams for the project, it works free of choice for ESP32 and ESP8266. The GPIOs for connecting the HX711 are chosen so that you do not hinder the start of the ESP8266 and have the same names for both controller types. Only the connections for the I2C bus are different, but are automatically assigned by the program.

Figure 6: HX711 scale on the ESP8266

Figure 6: HX711 scale on the ESP8266

Figure 7: HX711 scale on the ESP32

Figure 7: HX711 scale on the ESP32

The Micropython module for the HX711

Like most sensor assemblies, the HX711 module also needs operating software. Unfortunately, the HX711 is not I2C-capable. The data transmission also takes place in a serial manner via the lines dot and dpclk. 24 bit plus 1 to 3 bit are always transferred for the selection of channel A or B and the setting of the reinforcement. The MSbit (Most Significant bit = high -quality bit) is the first bit the HX711.

For programming the module hx711.py do I have this The manufacturer's data sheet used.

 From time import Sleep_us, Ticks_MS
 
 class Devicenotready(Exception):
     def __init__(self):
         print("Error \ nhx711 does not answer")

The exception class deals with the case that the HX711 is not accessible. This is followed by the declaration of the HX711 class, that of Devicenotready inherits. With the throwing of the exception, an instance is generated and the constructor ensures the output of the error message.

 class HX711(Devicenotready):
     Ksela128 = const(1)
     Kselb32 = const(2)
     Ksela64 = const(3)
     Dbits =const(24)
     Frame = const(1<<Dbits)
     Readydydelay = const(3000) # MS
     Waitsleep =const(60) # US
     Channelandgain={
         1:("A",128),
         2:("B",32),
         3:("A",64),
        }
     Calibration factor=2205.5

We start with a few constants. The calibration factor is determined by some weighings with various known massage pieces and a calculation tool, for example Libre Office. I will come later.

     def __init__(self, dot, pdsk, ch=Ksela128):
         self.data=dot
         self.data.init(Fashion=self.data.IN)
         self.CLK=pdsk
         self.CLK.init(Fashion=self.CLK.OUT, value=0)
         self.channel=ch
         self.tar=0
         self.cal=HX711.Calibration factor
         self.WAITREADY()
         K,G=HX711.Channelandgain[ch]
         print("HX711 ready on channel {} with gain {}".\
               format(K,G))

The constructor takes the two pin objects dot and pdsk, as well as the optional channel in ch. Dout is switched as an entrance, because it should receive the data from the outcome of the HX711. The ESP specifies the pace via the DPSCK management. We declare the attributes channel, tar and cal, then we wait for the Ready signal of the HX711. If it doesn't come, then throws WAITREADY() one Devicenotready-Exception. If it worked, then we get the channel and gain setting and issue a message in the terminal. The Dictionary Channelandgain convert the channel number into plain text.

     def Time-out(self,T):
         begin=Ticks_MS()
         def compare():
             return intimately(Ticks_MS()-begin) >= T
         return compare

The Closure Time-out() realizes a software timer who does not block the program as it is sleep() & Co.. The returned function compare() If a identifier is assigned to be asked about whether the time passed on has already expired in milliseconds (True) or not (false).

     def ISDEVICEREADY(self):
         return self.data.value() == 0

The method ISDEVICEREADY() returns True when the data line is on GND potential. According to the data sheet, this is the condition if the HX711 is ready to send data. With the first positive flank on the clock line, the HX711 provides the MSbit on the DOUT line.

Figure 8: Logic 2-scan

Figure 8: Logic 2-scan

With each additional pulse, the bits are pushed out in turn. In the meantime, the ESP must read and process the condition of the line. The ESP sets the pace through the clock. The pulse sequence is 125µs, which corresponds to a clock at approx. 8 kHz.

I have the pulse consequences with a logic analyzer and the software Logic 2 recorded. Whenever there are problems with data transmission, I like to use the DSO (digital memory oscilloscope), or a smaller tool that is cheaper for worlds Logic analyzer (La) with 8 channels. The thing is connected to the USB bus and shows using the Free softwarewhat is going on on the bus pipes. Where the shape of impulses does not matter, but only on its time sequence, a LA is worth gold. And while the DSO only provides snapshots of the curve, you can feel the LA over a long time and then zoom yourself into the interesting places. You can find a description of the device in the blog post "Logic Analyzer Part 1: Make I2C signals visible"By Bernd Albrecht. There it is also described how to scan the I2C bus.

After reading the 24 data bits, one to three more pulses are used pdsk issued, the tax bits. You have the following meaning:

Figure 9: Meaning of the control pulses

Figure 9: Meaning of the control pulses

     def WAITREADY(self):
         delayover = self.Time-out(Readydydelay)
         while need self.ISDEVICEREADY():
             IF delayover():
                 raise Devicenotready()

The name is program, WAITREADY() waiting for a true of ISDEVICEREADY(), but previously put the timer on the value in Readydydelay, that's three seconds. If Dout does not go to low during this time, one will be one Devicenotready-Sexception thrown. This brings off the calling program if it does not intercept the exception.

     def Conpricerult(self,Val):
         IF Val & Minval:
             Val -= Frame
         return Val

The HX711 sends the data as a two-complement values. Conpricerult () actually recognizes negative values ​​in the set MSbit, the bit 23. If it is set, the level 2 is the level 2 of the24 Subtracted to really get a negative number.

0xc17ac3 = 12679875 has a set MSbit

12679875 - 0x1000000 = -4097341

     def clock(self):
         self.CLK.value(1)
         self.CLK.value(0)

clock() just generates a positive pulse of 12.4µs on the PDSCK line.

     def channel(self, ch=None):
         IF ch is None:
             ch,gain=HX711.Channelandgain[self.channel]
             return ch,gain
         Else:
             assert ch in [1,2,3],\
             "Wrong channel number: {} \ n \
  Correct is 1.2 3 ".format(ch)
             self.channel=ch
             IF need self.ISDEVICEREADY():
                 self.WAITREADY()
             for n in range(Dbits + ch):
                 self.clock()

You overlook channel() No argument, then the function delivers the current channel and gain values. The Dictionary Hx711.channelandgain Take over the translation into plain text.

Once a channel number has been handed over, we check for the correct area, check whether the HX711 is ready and then push the corresponding number of pulses onto the PDSCK line, 24 plus the tax bits.

     def traw(self, conv=True):
         IF need self.ISDEVICEREADY():
             self.WAITREADY()
         raw = 0
         for B in range(Dbits-1):
             self.clock()
             raw=(raw | self.data.value())<< 1
         self.clock()
         raw=raw | self.data.value()
         for B in range(self.channel):
             self.clock()
         IF conv:
             return self.Conpricerult(raw)
         Else:
             return raw
The raw values ​​that the HX711 delivers are by traw() received. Becomes True Or not handed over an argument at all, then the return value is pink, i.e. an integer with a sign. With false the value Bloody returned, as it corresponds to the bit sequence, raw.

We are waiting for the sending of the HX711 and set the receiving buffer to 0.

In the for loop we send 23 pulses on PDSCK. With each run, we are in the place of the LSBIT (LEAST Significant bit = low -quality bit) the condition on the data line and then push the bits by one position to the left. The first received bit moves to position 23, the MSbit. After another clock pulse, the HX711 pushes the LSbit onto the data line, which we only on the LSbit of raw or have to. In addition to competition, the tax bits for the next measurement must then be clocked.

     def mean(self, n):
         S=0
         for I in range(n):
             S += self.traw()
         return intimately(S/n)

To smooth the noise of the values, we not only use a single measured value, but the mean from several measurements. That gets the method mean(), which we hand over the number of individual measurements.

     def tara(self, n):
         self.tar = self.mean(n)

The unpolluted scale naturally also delivers an ADC value, the Tara. We always call the method when booting the weighing program in order to be able to set the display to 0. The Tara value is in the attribute self.are saved so that the method Dimensions() has available, n is again the number of individual measurements.

     def Dimensions(self,n):
         G=(self.mean(n)-self.tar) / self.cal
         return G

The method Dimensions() pulls the Tara off the measured value and then calculates the true measured value in grams by division with the calibration factor. I will show how it is determined later.

     def Cal factor(self, F=None):
         IF F is need None:
             self.cal = F
         Else:
             return self.cal

During the calibration, it is an advantage if you can handle the calibration factor manually without having to newly upload the HX711.PY module to the ESP every time. The current value will be returned without an argument.

     def wake up(self):
         self.CLK.value(0)
         self.channel(self.channel)
 
     def tosleep(self):
         self.CLK.value(0)
         self.CLK.value(1)
         Sleep_us(Waitsleep)

wake up() and tosleep() tickle the HX711 from sleep mode, or put it in it. The signal sequence on the lines is specified by the data sheet of the HX711.

A big character set for the OLED display

Large digits make it easier to read the measurement. Instead of the usual eight pixels, we use 30 as a sign. The file geometer_30.py Contains the relevant information.

And so you make your own character set from the TTF supply from Windows.

Download the archive zeichensatz.rar Down and unpack the content into any directory. To save tipping, I recommend a list with a short name in the root path of the hard drive or a stick. With me it is F: \ fonts.

Open a PowerShell window in this directory from the Explorer by making a right click on the directory with a push button pressed. Then left click on Open the PowerShell window here.

Figure 10: Open PowerShell window

Figure 10: Open PowerShell window

Enter the following line at the prompt and press Enter:

. \ Makecharset.bat Britannic 30 "" 0123456789,-+KG "" "F: \ fonts \ sources \"

Figure 11: The character set is ready

Figure 11: The character set is ready

There is now a file in the directory britannic_30.py With the pixel data of the new character set. Only the characters were implemented in "" "0123456789,-+KG" ", that saves storage space. You can get other sign rates from the fonts-List of Windows in the directory sources Copy and convert as specified above. Please note that the file name is specified without the addition .TTTF.

The operating software of the Libra

The program scale.py accesses four external modules that have to be uploaded to the flash of the ESP before starting, hx711.py, oled.py, SSD1306.PY and geometer_30.py.

 From time import sleep
 From OLED import OLED
 import Geometer_30 AS CS
 import sys
 From HX711 import HX711

About the variable sys.platform an ESP can determine its himself. Then the GPIO pins are declared for the I2C bus.

 IF sys.platform == "ESP8266":
     I2C=Softi2c(scl=Pin code(5),sda=Pin code(4),freq=400000)
 elif sys.platform == "ESP32":
     I2C=Softi2c(scl=Pin code(22),sda=Pin code(21),freq=400000)
 Else:
     raise Unkownporterror()

We instantiate a display object, delete the display and declare the PIN objects for the connection to the HX711, also button, the instance for the Tara key.

 IF sys.platform == "ESP8266":
     I2C=Softi2c(scl=Pin code(5),sda=Pin code(4),freq=400000)
 elif sys.platform == "ESP32":
     I2C=Softi2c(scl=Pin code(22),sda=Pin code(21),freq=400000)
 Else:
     raise Unkownporterror()

The function Putnumber() positions the pixel pattern of the character with the number n (based on the extract generated above, not Ascii). The position of the top left corner of the pattern is in XPOS, YPOS.

 def Putnumber(n,XPOS,YPOS,show=True):
     width=CS.number[n][0]
     for row in range(1,CS.Height):
         for col in range(width-1,-1,-1):
             C=CS.number[n][row] & 1<<col
             D.setpixel(XPOS+width+3-col,YPOS+row,C,False)
     IF show:
         D.show()
     return XPOS+width+2

A look at the file geometer_30.py explains the procedure. In the string chars the characters are listed. The index in the string is the drawing number. The "-" sign is therefore number 11, the "0" is number 0. In the list number this number identifies the Tupel With the width information and the pixel matrix. This is now scattered by line column for columns and the pixel is set when a 1 has been found. That happens hidden. Only if show the value True Has, the entire buffer is sent to the display. The return value is the next free drawing position.

Figure 12: Matrix for the sign 0

Figure 12: Matrix for the sign 0

The drawing position is set to 0, as a start information I give a number of "-" signs. Then the ESP tries to contact the HX711. If this succeeds, we wake it up, channel A with Gain 128 and see the TARAWALT that in the attribute self.are lands.

 POS=0
 try:
     for n in range(8):
         POS=Putnumber(11,POS,0)
     HX = HX711(dot,dpclk)
     HX.wake up()
     HX.channel(1)
     HX.tara(25)
     print("Libra started")
 except:
     print("HX711 does not initialize")
     for n in range(8):
         POS=Putnumber(0,POS,0)
     sys.exit()
     
 freq(160000000)

If the connection does not work, we get a message in the terminal and a series of zeros in the display.

160MHz is for the ESP8266 Topspeed, the ESP32 also packs 240 MHz.

Then it goes into the main loop. We see if the Tara key is pressed, confirm this with a series of commas on the display and get a new Tara value. This feature is useful because the weighing cell is subject to a temperature drift and the zero point can be readjusted at any time.

 while 1:
     IF button.value() == 0:
         D.clearall(False)
         POS=0
         for n in range(14):
             POS=Putnumber(10,POS,0)
         HX.tara(25)

The format string for the output is fed with the measured value. Disconnect the display, delete the drawing position to 0, decimal point with a comma and write the characters in the buffer up to the penultimate.

     M="{: 0.2f}".format(HX.Dimensions(10))
     D.clearall(False)
     POS=0
     M=M.replac(".",",")
     for I in range(len(M)-1):
         Z=M[I]
         n=CS.chars.index(Z)
         POS=Putnumber(n,POS,0, False)

With the output of the last character, the buffer content is sent to the display. The next round starts after half a second break.

     Z=M[len(M)-1]
     n=CS.chars.index(Z)
     POS=Putnumber(n,POS,0)
     state=(button.value() == 0)
     sleep(0.5)

You can download here.

Calibrate the scale

Figure 13: Mass pieces for calibration of the scale

Figure 13: Mass pieces for calibration of the scale

You need a few mass pieces and, if necessary, a kitchen or letter scale if the mass pieces are not calibrated. In this case, you can make a lay of fat nuts or screws or the like. Of course, the masses must be determined with a different scale before use.

Figure 14: Calibrate on the ESP8266

Figure 14: Calibrate on the ESP8266

Start the program now scale.py In Thonny and break in the main loop Ctrl+C away. The HX711 object is under HX instantiated and accessible from the terminal console. We start with the following commands.

 >>> HX.tara(50)
 >>> HX.tar
 562708
 >>> HX.mean(25)-HX.tar
 -17
 >>> HX.mean(25)-HX.tar
 8

We subtract the Tara value from the average of 25 individual measurements. The results should be between -50…+50 if the scales are not loaded.

Now we lay the mass pieces one after the other and in groups, so that it is increasingly stressed. Every time we start the last command again and write down the mass in Grams and the value displayed in the terminal.

The Table Let us enter in Libre Office Calc or another program and display the measurement curve together with the determination (correlation coefficient) and the formula.

Figure 14: Calibration curve

Figure 14: Calibration curve

The correlation coefficient R2 Is in fact 1, that speaks for the precision of our measurement. We can neglect the axis section of -43, ... with an order of 100,000. The slope factor 1104 of the straight line is our sought -after calibration factor. Now share this value to the object HX with.

 >>> HX.Cal factor(1104)
 >>> HX.Cal factor()
 1104

If you are now the method Dimensions() Call up, a value of 0.0 ... should be displayed.

 >>> HX.Dimensions(25)
 0.0244565

Wear the calibration factor in the file hx711.py one, upload them to the ESP and start the program scale.py new.

 Calibration factor=1104
If the mass is displayed when the masses are put on, they have won and can pound on the shoulder.

If the displayed value is slightly next to the expected weight, you can still file a little on the calibration factor value. If you enlarge it, the mass value will fall and vice versa.

In the second part I show you how I exchanged the OLED for a large LED display. Until then.

DisplaysEsp-32Esp-8266Projekte für fortgeschritteneSensoren

4 comments

Guido

Guido

Hi, tolles Projekt. Habe ich gleich versucht zu verwirklichen. Komme aber absolut nicht mit der Kalibrierung klar. Könnte ich da eine Anleitung für absolute Dummys bekommen? Vielen Dank und Grüße
Guido

Jürgen

Jürgen

@Udo
In der Tat ergibt sich eine leichte Abweichung in der Ablesung. Ob dabei ein Drehmoment oder eine Hebelwirkung eine Rolle spielt, ist schwer feststellbar. Mit einem 2000g-Massenstück habe ich bei meiner großen Waage Abweichungen von +/-4g, wenn ich die Masse am Rand des Tellers platziere und +/-2g beim Aufsetzen am rechten und linken Rand. Ähnlich verhält es sich auch mit kommerziellen Waagen, die nachdemselben Prinzip arbeiten, weshalb empfohlen wird, die Massen mittig aufzulegen.

Marte Schwarz

Marte Schwarz

Hallo,
ich möchte dezent auf den Unterschied von Auflösung und Genauigkeit hinweisen.
Zum anderen möchte ich darauf hinweisen, dass mir bisher kein fehlerfreies Hx711 Modul in die Finger kam. Meist fehlt eine Masseverbindung zur Brücke, die Widerstände sind falsch berechnet und machen so den internen Spannungsregler unwirksam und die Kondensatoren, die im Datenblatt vorgeschlagen werden, sind selten auf den Boards auch in der richtigen Größe zu finden.
Ich gebe zu, das Hx711-Modul von AZ hatte ich noch nicht in der Hand.
Gruß
Marte

udo

udo

Wundert mich, dass das funktioniert?
Bei der Konstruktion spielt doch die Position eine Rolle, Wo das Gewicht plaziert wird. Rechts von der Schraube hat man ein rechtsdrehendes-, links von der Schraube ein linksdrehendes Drehmoment. Kann eigentlich nicht sein, dass da das gleiche Gewicht ermittelt wird?
Oder mach ich einen Denkfehler?
Gruß Udo

Leave a comment

All comments are moderated before being published

Recommended blog posts

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery