Mammut-Matrix Display mit MAX7219, ESP8266/ESP32 und GY-21 in MicroPython - Teil 2

This blog is also available as PDF document.

A nice large display can't be everything. After all, you don't buy a car just to put it in the garage so that it doesn't feel alone. So in this episode I will present you some nice application examples of the Mammut matrix displays from part 1. Because of the few components, this project, as well as the Matrix display itself, is well suited for the beginners. I start today with a small weather station made of only two parts, not counting the display, which was already discussed in detail in the previous episode, together wit the MicroPython module matrix8x8.py

Thermal hygrometer

Thermal hygrometer

In this episode you will also learn how a CRC test sum is calculated and how a CRC test works. But, before we get started, welcome to


MicroPython on the ESP32 and ESP8266

today

The Mammut matrix display and the HTU21

The HTU21 is a dwarf of 3mm x 3mm, which is located on a platinum with 10mm x 13mm, which bears the name GY-21. On the underside of the circuit board, I discover a voltage controller and diverse "chicken food". The building block can therefore be supplied with voltages of 5V, but works with 3.3V. This is very good, because this lies the level on the I2C bus lines with which the part is controlled, in the safe area for the ESP32. He already has the task of heading for the display and now chatting the additional job with the HTU21 alias SHT21.

Figure 1: HTU21 alias GY-21

Illustration 1: HTU21 alias GY-21

The topics of these conversations will be the temperature and the relative humidity. The result of the conversations should then land on the matrix display. The latter should already consist of at least 8 elements for this purpose.

Figure 2: moisture indicator

Illustration 2: Humidity indicator

The data sheet of the HTU21 can be found for example here or here.

With regard to the controllers, ESP32 and ESP8266, I was interested to what extent both families are suitable for the tasks mentioned. With one exception, both have a SPI bus and an I2C bus and have therefore proven to be easy to use. The exception is the small ESP8266-01. Only GPIO0 and GPIO2 are led with him. So you could flang an HTU21, which we will do in the next episode.

Hardware

1

D1 Mini Nodemcu with ESP8266-12F WLAN module

Nodemcu Lua Amica Module V2 ESP8266 ESP-12F

ESP32 Dev Kit C unpleasant

ESP32 NODEMCU Module WiFi Development Board

Nodemcu-ESP-32S kit

upon need

Max7219 8x8 1 Dot Matrix MCU LED display module

Max7219 8x32 4 in 1 Dot Matrix LED display module

1

GY-21 HTU21 moisture and temperature sensor

various

Jumper cable

1

Minibreadboard or

Breadboard kit - 3 x 65stk. Jumper Wire Kabel M2M and 3 x mini Breadboard 400 pins

1

Plywood strips 5x 25cm ...

When Iining up matrix units, individually or in groups, a plywood strip of 5cm wide and the length of the entire display has proven itself. I fixed the boards with Eternit sealing mass. The plasticine holds where it should, but can also be completely removed.

The switching sketches for the ESP32 and the ESP8266 are very manageable.

Figure 3: SHT21-Thermo+Hyrdometer with ESP32

Illustration 3: SHT21-Thermo+Hyrdometer with ESP32

Figure 4: SHT21-Thermo+Hyrdometer with ESP8266

Illustration 4: SHT21-Thermo+Hyrdometer with ESP8266

An Amica Node-MCU fits on a mini-breakboard. The same applies to an ESP32S that has a narrower footprint than, for example, an ESP32 Dev Kit C V3

Figure 5: ESP32S with GY-21

Illustration 5: ESP32S with GY-21

Figure 6: Amica Node-MCU from the ESP8266 family

Illustration 6: Amica Node-MCU from the ESP8266 family

The Software

For flashing and the programming of the ESP32:

Thonny 

µpycraft

Used Firmware for the ESP8266/ESP32:

Micropython firmware

Please choose a stable version

ESP8266 with 1MB 

ESP32 with 4MB 

The MicroPython programs for the project:

matrix8x8.py 

matrixtest.py 

sht21.py 

sht21_32_test.py 

sht21_8266_test.py 

shtdisplay.py 

MicroPython - Language - Modules and Programs

For the installation of Thonny you will find here a detailed instructions (English version). In it there is also a description of how to burn the Micropython firmware (state 05.02.2022) on the ESP chip.

MicroPython is programming language. The main difference to the Arduino IDE, where you always and only flash entire programs, is that you only need to flash the MicroPython Firmware once at the beginning on the ESP32, so that the controller understands MicroPython instructions. You can use Thonny, µPyCraft or esptool.py for this purpose. For Thonny I have described the process here.

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 transfer 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 up 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 blanks file. 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

Manually, programs are started from the current editor window in the Thonny IDE via the F5 key. This is faster than clicking 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 the meantime, Arduino IDE again?

If you want to use the controller together with the Arduino IDE again later, just flash the program in the usual way. However, the ESP32/ESP8266 has then forgotten that it ever spoke MicroPython. Conversely, any Espressif chip that contains a compiled program from the Arduino IDE or the AT firmware or LUA or ... can easily be provided with the MicroPython firmware. The process is always as here described.

Signals on the I2C bus

In the past episodes on the subject of I2C bus or SPI bus, I sometimes recorded and mapped the signals on the bus lines with the DSO (AKA digital memory oscilloscope). I recently received inquiries from readers who had problems with the I2C bus. The occasion was a defective cable in one case, in the other case it was bad contacts. The errors were quickly limited by a few MicroPython commands. Other mistakes are more stubborn.

During the preparation of this article I had a HTU21 in use, which could be localized on the I2C bus without problems. But it strictly refused to read out its register contents. Changes of my driver software were unsuccessful. In such cases I then use the DSO, or a much cheaper, small tool, a Logic analyzer (LA) with 8 channels. The thing is connected to the USB bus and shows by means of a free software what is going on the bus lines. Where the form of pulses is not important, but only their timing, an LA is extremely valuable. And, while the DSO only provides snapshots of the curve, with the LA you can sample over a longer period of time and then zoom in on the interesting parts. By the way, you can find a description of the device in the blogpost Logic Analyzer - Part 1: Make I2C signals visible by Bernd Albrecht. There it is also described how to scan the I2C bus.

Well, after a few hours of data sheet studies and unsuccessful troubleshooting I also used the program. If I had only done that much earlier ... in short, the situation was clear. This is how the curve shapes in the defective HTU21 look:

Figure 7: Addressing of the HTU21 with nack response

Illustration 7: Addressing of the HTU21 with NACK response

And this is what it should have looked like. Do you see the difference in the first frame? The hardware address is already acknowledged with a NACK instead of an ACK bit

Figure 8: Order for a temperature scan

Illustration 8: Order for a temperature scan

And so the reading command should have looked like, which did not happen due to the error in the chip.

Figure 9: Coll up the temperature raw value 91ms later

Illustration 9: Coll up the temperature raw value 91ms later

Instead, the faulty module is again a NACK (Not Acknowledge), which is caused by the fact that the slave, here the HTU21, does not pull the SDA line from the controller during the ninth clock pulse. Fatally, in a relaxed episode, a field came in the expected position, but mostly not. You are looking for a wolf. The problem was solved with the exchange of the building block.

Figure 10: Reading command on the HTU21 with nack

Illustration 10: Reading command on the HTU21 with NACK

The SHT21 class

After this short excursion to troubleshooting, we now build the SHT21 class. Compared to the MATRIX class, it is much narrower. Again, the SHT21 data sheet is the source of all wisdom.

I import the class I2CBus and the function sleep_ms(). In I2CBus I have declared a set of methods that unify the parameter passing to the methods of the Softi2c class. The conversion from the number formats byte and integer to the type bytearray is done there automatically.

from I2CBus import I2CBus
from time import sleep_ms

class SHT21(I2CBus):

My class SHT21 inherits from the class I2CBus. Thus all methods of I2CBus are available in the namespace of SHT21 without Prefix. It is not to be expected that an identifier in SHT21 could overwrite a namesake in I2CBus.

In the section 5.3 Singing A Command on page 7 of the data sheet I find the hardware address of the SHT21. 

After sending the Start Condition, the subsequent I2C

header consists of the 
7-bit I2C Device Address ‘1000,000’

and an SDA direction bit (Read R: '1 ’, Write w:‘ 0 ’).

Like most I2C devices, the hardware address (Device Address) is a 7-bit value here. 0B1000000 = 0x40 = 64. By adding the directional bits R/-W at the LSB position, it becomes the 8-bit address that we see on the plots in Figures 7 to 10. The SHT21 gropes the SDA line when the flank rises to SCL. Eight rising flanks for 8 bits, the ninth rising flank reads the ACK (0) or Nack-Bit (1) on the SDA line.

On page 8 in the data sheet, at the top left, the table follows the register of the SHT21. I transfer them to programlisting accordingly.

Figure 11: Register or command table

Figure 11: Register or command table

    Hwad=const(0x40)
   # Commands
   Triggered = const(0xe3)
   Triggerrh= const(0xe5)
   Triggerto=const(0xf3)
   Triggerrhnohold=const(0xf5)
   Writeuser=const(0xe6)
   Readuser=const(0xe7)
   Soft reset=const(0xfe)

The constructor needs the I2C object created in the main program and optionally takes a hardware address. Its value is preassigned by default by the constant HWADR and also immediately transferred to an instance variable self.hwadr so that it is available to all methods of the class.

The next command super().__init__(i2c,self.hwadr) instantiates the class I2CBus. It corresponds to the otherwise necessary constructor call I2CBus(i2c,0x40).

The following are the declarations for the waiting times after sending a transducer command for temperature and relative humidity until the values are read in. I take these from the columns RH max and T max from table 7 on page 9 in the data sheet.

Figure 12: converter waiting times

Illustration 12: Converter waiting times

        self.Twait=(86,22,43,11) # MS
       self.Hwait=(30,4,9,15) # MS
       self.reslags=(0x00,0x01,0x80,0x81)
       self.residual text=("12/14", "8/12", "10/13", "11/11")
       self.resolution=0 # RH/T = 12/14

There are four combinations for the resolution of the temperature and humidity measurement. Bit 7 and bit 0 in the user register are responsible for this. These combis are in the resFlags tuple. resText specifies the combis in plain text and resolution holds the current resolution setting. The info for this is in Table 8 in the data sheet, page 9.

Figure 13: User registers and converter resolution

Illustration 13: User register and converter resolution

        self.SHT21reset()
       self.Traw=0
       self.Hraw=0
       self.Tempo=0
       self.Humid =0
       print("Sht21 Initialized @ {: #x}".format(self.hwad))
       sleep_ms(15)

Then I reset the SHT21 with my routine sht21Reset() and create the global variables for temperature and humidity. The edition of the constructor follows. Then we are waiting for the reset command. Section 5.5 on page 9 informs about this.

The soft reset takes less than 15ms.

The method for the chip reset consists of a write command to the SHT21. We access the register SoftReset = 0xFE writing as specified in the data sheet in section 5.5 on page 9.

Figure 14: Reset command

Illustration 14: Reset command

    def SHT21reset(self):
       self.Writetobus(Soft reset,self.hwad)


The conversion of a temperature value and the reading in is done by the method
readTemperatureRaw().

    def Reading temperatureeraw(self):
       self.Writetobus(Triggerto,self.hwad)
       sleep_ms(self.Twait[self.resolution])
       data=self.ReadbyesFrombus(3,self.hwad)
       IF self.calcchecksum(data,3) == 0:
           self.Traw=data[0]<<8 | (data[1] & 0xFC)
       Else:
           self.Traw=0
       return  data

And that's what's behind it - data sheet page 9:

Figure 15: Process of a conversation

Illustration 15: Process of a conversation



I send the command to start a temperature measurement, TriggerTnoHold = 0xF3. noHold refers to the behavior of the SCL line during the measurement and says that SCL is not held at GND level by the SHT21. So the bus remains free during the measurement time.

Either I ask again and again whether it is already so far, or I simply wait the time given by Sensirion. I have chosen the second solution and send the ESP to sleep for a few milliseconds. How long that is depends on the current resolution setting in resolution. After that three bytes are fetched from the bus, which the SHT has to send. If the checksum, the third byte, is 0, I compose the raw value of the temperature from the first two bytes. data[0] contains the MSB. I shift the bits 8 places to the left, which corresponds to a multiplication by 256 and oriere with the LSB in data[1], from which I delete the two least significant bits. This is written on page 8 in the penultimate paragraph:

For the calculation of physical value status bits must be set to '0' - See Chapter 6.

If there is a transmission error, I set TRaw to 0

This is followed by the calculation of the true temperature, as specified on page 10 of the data sheet. I also make the result round in two places after the comma. To do this, I multiply the calculated value by 100, add up 0.5, so that 5 is correctly rounded up and make a total of 100 parts out of the sum.

    def calcare(self):
       self.Tempo=intimately((-46.85+175.72*\
                      self.Traw/pow(2,16))*100+0.5)/100
       return self.Tempo 

For the relative humidity, the processes are the same, only the formula for calculating the final value is different.

The procedure for the calculation of the checksum (crc = cyclic redundancy check) I have described exactly in Crc8 calculation.pdf with an example. Here it is applied for three bytes in a row. Because the crc byte transmitted by the SHT21 is integrated in the check, the result must be 0x00. data gives the byte array and nob the number of bytes to be tested (number of bytes) to the method. With this test method the remainder of a polynomial division is determined by continued subtraction. Each occurring power in the test polynomial and in the remainder corresponds to a 1.
 

    def calcchecksum(self,data,nob):
       poly=0x131 # Polynomial: x^8+x^5+x^4+1 = 100110001
       CRC=0
       for I in range (nob):
           CRC ^= data[I]
           for POS in range(8,0,-1):
               IF CRC & 0x80:
                   CRC = (CRC << 1) ^ poly
               Else:
                   CRC = (CRC << 1)
       return CRC

Remember me to carry out a test of this method if everything is remaining in the box.

To be able to change the resolution, I must have read and write access to the user register. This is done by the two relevant methods readUser() and writeUser(). A bytes object is read from the bus and returned as an integer by readUser().

    def readuser(self):
       self.Writetobus(Readuser,self.hwad)
       data=self.ReadbyesFrombus(1,self.hwad)# Bytes object
       return data[0] # Contents user register as an integer
    def writeuser(self,data):
       r=self.writeby(Writeuser,data,self.hwad)
       return r


The method resolve() accesses the two mainzelmännchen. The data sheet explains on page 9 in section 5.6 that the reserved bits 3, 4 and 5 must not be changed. Therefore, I read the user register , clear bits 7 and 0, and ORing them with the pattern fetched by the index res from the tuple resFlags. If the res parameter is not specified, then resolve() queries the current resolution and returns the index. 

    def resolve(self, res=None):
       W=self.readuser()
       IF res is need None:
           assert 0 <= res <= 3
           W &= 0x7e
           W |= self.reslags[res]
           self.writeuser(W)
           self.resolution =res
           return res
       Else:
           W &= 0x81
           return self.reslags.index(W)


The method tellResolution() goes one step further. It returns the index and the plaintext message from the resText tuple.

    def Tell resolution(self):
       return self.resolution,self.residual text[self.resolution]

The user register can also provide information about the battery status. This may be helpful if the power supply does not come from a power supply unit. If the voltage drops below 2,25V, bit 6 goes to 1. The query is done by the method tellBattStatus(). I get the register content and rapid BatteryMask=0x40 and move the bit in position 6 to position 0. This gives me an index 0 or 1 that I can use as a pointer into the tuple ("OK", "BAD").
    def Tellbatt status(self):
       W=(self.readuser() & Battery mask)>>6
       return ("OK","BATH")[W]


Good, everything in the box, then you have now either entered the program text of the module sht21.py itself or you have already downloaded it before or just now. If not, then you should do so now at the latest. Because, wasn't there something else you should remind me of? Exactly, the test of calcChecksum(). But it doesn't work that quickly.

First you have to upload the module sht21.py into the flash of the ESP. Surely you have already built the circuit? Then you need a test program that does the extensive preliminary work so that you don't have to enter the many lines by hand each time. For an ESP32 the program is called sht21_32_test.py and for an ESP8266 it is sht21_8266_test.py. The difference is only in the pins for the I2C bus.

# sht21_32_test.py
from machine import Pin code, Softi2c
from I2CBus import I2CBus
from time import sleep_ms
from SHT21 import SHT21
import OS,sys
button=Pin code(0,Pin code.IN,Pin code.Pull_up)
blinkled=Pin code(2,Pin code.OUT)

# Pintranslator for ESP8266 boards
# Lua-Pins D0 D1 D3 D4 D5 D6 D7 D8

# SC SD

Scl=Pin code(21)
Sda=Pin code(22)
I2C=Softi2c(Scl,Sda)

sleep_ms(15) # For Booting SHT-Device from Power Up
S=SHT21(I2C)
S.Reading temperatureeraw()
print(S.calcare(),"° C")
S.Readhumidacyraw()
print(S.Calchumidity(),"%RH")

Open the program in an editor window and start it with F5. If you have done everything right, you will get the first message from the HTU21.

>>> %run -C $Editor_content
I2CBus-Tool OK @ 0x40
SHT21 Initialized @ 0x40
20.78 ° C
36.82 %Rh


Now we test the crc function. Assume that the HTU wants to send bytes 37 and 142 for the temperature. Then it must calculate a checksum. It does that just like the calcChecksum() method. We create a byte array with these values because we also get such a data type from HTU21. We pass this to the calcChecksum() method. s is the HTU21 or SHT21 object. 206 is the checksum of the two bytes.

>>> data=bytearar([37,142])
>>> S.calcchecksum(data,2)
206

The SHT21 hangs the 206 on the other bytes on it and sends it to us. We take the bytolge and chase them through the redundancy check. Isn't that a great magic? In fact, 0 comes out, so no mistake in transmission.

>>> data=bytearar([37,142,206])
>>> S.calcchecksum(data,3)
0

The climate program with a large display

Maybe you wonder why I knitted a module with a SHT21 class. Wouldn't it have been enough to simply incorporate all the methods into a corresponding program as functions? There are at least two good reasons in favor of the class solution.

  • Modules and classes are universally reusable.
  • Modules and classes detoxify a program and make it easier to read, clearer and easier to maintain.
  • Both Matrix and SHT21 will reappear as classes in two other episodes in this series. The import is done with a line. Otherwise I would have to copy a variety of program lines.
  • And what if I want to change a function and used the package in 20 other programs? Then I would have to change the function in 20 programs. As a method in one class, I do this exactly once and the change is transparently adopted in 20 programs.

Now you not only have two, but four good reasons and there are more!

Well now let's put everything together into one program, for ESP32 and ESP8266 and the matrix display and the HTU21 sensor: SHTDISPLAY.py

# shtdisplay.py
import sys, OS
from time import sleep_ms,sleep
from matrix8x8 import MATRIX
from machine import Pin code, Softi2c, Spi
from I2CBus import I2CBus
from SHT21 import SHT21

chip=sys.platform
IF chip == 'ESP8266':
   # Pintranslator for ESP8266 boards
   # Lua-Pins D0 D1 D3 D4 D5 D6 D7 D8
   
   # SC SD
   bus = 1
   Misop = Pin code(12) # D6
   Mosip = Pin code(13) # D7
   SCKP = Pin code(14) # D5
   spi=Spi(1,baud rate=4000000)   #ESP8266
   # # alternatively virtual with bitbanging
   # spi=SPI(-1,baudrate=4000000,sck=SCK,mosi=MOSI,\
   #         miso=MISO,polarity=0,phase=0) #ESP8266
   CSp = Pin(16, mode=Pin.OUT, value=1) # D0
   SCL=Pin(5) # D1
   SDA=Pin(4) # D2
elif chip == 'esp32':
   bus = 1
   MISOp= Pin(15)
   MOSIp= Pin(13)
   SCKp = Pin(14)
   spi=SPI(1,baudrate=10000000,sck=Pin(14),mosi=Pin(13),\
           miso=Pin(15),polarity=0,phase=0)  # ESP32
   CSp = Pin(4, mode=Pin.OUT, value=1)
   SCL=Pin(21)
   SDA=Pin(22)
   taste=Pin(0,Pin.IN,Pin.PULL_UP)
   blinkLed=Pin(2,Pin.OUT)
else:
   # blink(led,800,100,inverted=True,repeat=5)
   raise OSError ("Unbekannter Port")
   
print("Hardware-Bus {}: Pins fest vorgegeben".format(bus))
print("MISO {}, MOSI {}, SCK {}, CS{}".format(MISOp,MOSIp,SCKp,CSp))
print("SCL {}, SDA {}\n".format(SCL,SDA))

numOfDisplays=16
d=MATRIX(spi,CSp,numOfDisplays)
i2c=SoftI2C(SCL,SDA)
sleep_ms(15) # for booting SHT-Device
s=SHT21(i2c)

delay=3

while 1:
   d.clear()
   s.readTemperatureRaw()
   tempString="{0:3.1f} C"
   humString="{0:3.1f} %"
   s.calcTemperature()
   print(s.Temp,"°C")
   d.text(tempString.format(s.Temp),0,0,1)
   d.show()
   sleep(delay)
   d.clear()
   s.readHumidityRaw()
   s.calcHumidity()
   print(s.Hum,"%RH")
   d.text(humString.format(s.Hum),0,0,1)
   d.show()
   sleep(delay)


The program has 84 lines. With all imports, this adds up to over 500 lines. Another good reason for modules and classes, right?

The most interesting line in this listing is

chip=sys.platform

This provides us with the type of the used controller. With this info it is easy to preassign the GPIO pins for SPI and I2C bus correctly before instantiating the corresponding objects with it. If the further program does not use any type-specific commands, you can use it for multiple controllers without having to make any adjustments to it.

One flaw is the size of this section. But maybe I have infected you with the class virus and you tinker until the next time a module with a class, whereby one can outsource this story. Have fun trying, researching and with the homework.

See you!

DisplaysEsp-32Esp-8266Projekte für fortgeschritteneSensoren

4 comments

Andreas Wolter

Andreas Wolter

@Werner Schrödter: Die Displaymodule verfügen über Ein- und Ausgänge. Man kann sie in Reihe schalten, also den Ausgang eines Moduls auf den Eingang des nächsten. Die Variable numOfDisplays (hier =16) sollte dabei die Anzahl der Displays sein.

Grüße,
Andreas Wolter
AZ-Delivery Blog

Werner Schrödter

Werner Schrödter

Hallo Herr Grzesina,
in Ihrer tollen Blog heißt es:

Natürlich ist das Display beliebig verlängerbar von einem Einzelelement bis zu
Kaskaden aus mehreren Vierergruppen.

Wo und wie genau kann man das einstellen ?

Mit freundlichen Grüßen ein zufriedener Kunde
Werner Schrödter

Jürgen

Jürgen

Hallo, Michael,
tut mir leid, dass ich erst jetzt antworten kann, ich war für zwei Wochen auswärts und bin erst gestern abend zurückgekommen. Zu deinen Problemen gibt es folgende einfache Lösungen.
attach-Problem:
Lösche bitte in zeile 69 folgenden Text “attach=True,” mitsamt dem Komma oder lade die von mir korrigierte Datei über den Link im Blogpost neu herunter.
i2cbus-Problem: Leider habe ich übersehen, in der Softwareliste den Link auf das I2C-Hilfs-Modul i2cbus.py einzutragen. Die Datei befindet sich in
http://www.grzesina.de/az/matrix/i2cbus.py und muss dann auch auf den ESP hochgeladen werden.

Du hast also gar nichts falsch gemacht, die Fehler lagen allein bei mir.

VG Jürgen

Michael Daiss

Michael Daiss

Hallo,
ich habe es mit einem ESP32 nach/aufgebaut.

Bei matrixtest.py laufen die Herzchen einmal durch, dann nach ca. 5 Sek. wieder zurück und
dann bricht es mit folgender Fehlermeldung ab:
File “”, line 69, in TypeError: unexpected keyword argument ‘attach’

Bei sht21_32_test.py kommt gleich folgende Fehlermeldung:
File “”, line 4, in ImportError: no module named ‘i2cbus’

Was kann ich falsch gemacht haben? VG Michael

Leave a comment

All comments are moderated before being published