Four years ago I had the Environment system called AZ-Envy tried out, with an ESP8266-12F, an MQ2 as a alleys and an SHT30 for recording temperature and relative humidity. The heating current thickness of the sensor was approx. 170mA at 5V. This results in an output of 850 MW! The energy supply was carried out via USB, but for programming you also needed a USB for TTL adapter.
A few days ago I learned about a successor version of the board. The AZ Onoboard comes back with an ESP8266 12F and now has real USB access with a CH340C. The board also has an optional connection for 5V, where the bus voltage also lies. In addition to the main board, the KIT also contains three breakout boards: a SHT30, a BH1750 (luxet meter) and a SGP30, the CO2, as well as the TVOC proportion (totally volatile organic compounds), i.e. fleeting organic compounds in the room air. The sensor contained in the SGP30 is smaller than the MQ2 and works at 1.8V with 48mA, which is just 86mw. The CO2 EQIUValent is measured in PPM (parts per million) and tvoc in ppb (parts per trillion).
The sensorboards, the chips of which are all controlled via the I2C bus, are infected via one of the socket strips on the AZ onboard. Other I2C bus modules, such as an OLED display, can of course also be connected to these socket strips. However, you have to use jumbling cables to adapt the pin assignment.
Figure 1: AZ-Eoneboard
Today we will take the BH1750 light sensor and eliminate a weak point of the AZ onboard. What it is about, what you can do and how the BH1750 is addressed via Micropython, you can find out in this episode from the series
Micropython on the ESP32, ESP8266 and Raspberry Pi Pico
today
A luxmeter with the ESP8266
I selected the Luxmeter as the first post because the associated Micropython module is not as extensive. This makes it easier for beginners and remain room for the expansion of the board and the integration of an OLED display. What is needed?
The hardware
The usual board with the controller, here an ESP8266, is replaced by the AZ onboard.
The structure is so simple that a circuit diagram is superfluous. We simply put the breakout board with the BH1750 on one of the boxes in the controller board, as shown in Figure 2.
Figure 2: AZ-ONEBARD with BH1750
The software
For flashing and the programming of the ESP32:
Thonny or
Signal tracking:
Used firmware for the ESP8266:
The micropython programs for the project:
timeout.py: Non-blocking software timer
oled.py: OLED-API
SSD1306.PY: OLED hardware driver
bh1750.py: Hardware driver module
bh1750_test.py: Demo program
bh1750_kal.py: Program for calibrating the Lux values
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. Like you that Raspberry Pi Pico get ready for use, you will find here.
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.
Preparation of the AZ onboard
The board is delivered with a sketch that checks the function of the sensors and the measured values in a terminal program such as Putty outputs. However, this only works if all three break out boards are infected.
Figure 3: Edition with the demosoftware
Even with our development tool Thonny, this works when we First connect the AZ-EONboard to the bus and then start Thonny.
But we want to write and run our own program in Micropython. The first step to the goal is that we burn a corresponding Micropython core on the ESP8266 that overwrites the demo program. So first load them Firmware down and then follow of these instructions. The ESP8266 then reports in Replica, Thonny's terminal, about Sun:
XXXXXXXXXX
Micropython V1.23.0 on 2024-06-02; ESP modules (1M) wither ESP8266
Type "Help ()" for more information.
>>>
Then it is already time for the first attempts at walking. Inputs in Replica Become in this script fat formatted, the answers from the ESP8266 italic. We want to see if the ESP8266 can contact the sensorboard. To do this we have to do the classes Pin and Softi2c import. Then we instance a bus object and use it for a round recall who is there.
XXXXXXXXXX
>>> From machine import Pin, Softi2c
>>> I2C=Softi2c(Pin(5),Pin(4),freq=100000)
>>> I2C.scan()
[35]
In the listthe function scan() returns, the hardware or device address of the BH1750 chip, 35 decimal or 0x23 hexadecimal can be found. So the connection works.
Now we connect our OLED display with four jumper cables on a different socket strip. The cables are necessary because the pins on the display have a different order than on the AZ-EONboard. The assignment is the following:
Display: AZ-Eoneboard
VCC: +
GND: -
SCL: SCL
sda: sda
Figure 4: OLED connected
XXXXXXXXXX
>>> From machine import Pin, Softi2c
>>> I2C=Softi2c(Pin(5),Pin(4),freq=100000)
>>> I2C.scan()
[35, 60]
We see that a second device with the address 60 = 0x3c has registered: the display. We also test that. But so that the next commands also work, we must first have the files oled.py and SSD1306.PY Upload to the flash of the ESP8266. We import the OLED class and create a display object.
XXXXXXXXXX
>>> From OLED import OLED
>>> D=OLED(I2C, Heightw=32)
This is the constructor of OLED class
Size:128X32
>>> D.writer("Luxmeter 1.0",2,0)
14
The text appears in the middle of the first display line because we wrote from column 2 and the text consists of 14 characters. A line holds a maximum of 16 characters.
Next we start the driver module for the BH1750.
The class BH1750
We have to do two things to access the inside of the BH1750. A story is the conversation with the chip via the I2C bus. The other, more extensive thing, is the control of processes on the chip, either by accessing the internal register (storage points), or, as here, the sending of commands and picking up the data. Both steps are usually dissolved differently from chip to chip. As always, we start with the import business.
XXXXXXXXXX
From machine import Pin, Softi2c
From time import Sleep_ms
class BH1750:
Hwad = const(0x23) # AdDR = low
Pwrdown = const(0x00)
Pwron = const(0x01)
Reset = const(0x07)
Conthres = const(0x10) # 120ms; 0.5 lux
Conthres2= const(0x11) # 120ms; 1.0 lux
Contlres = const(0x13) # 16ms; 4.0 LX
Oncehres = const(0x20)
Oncehres2= const(0x21)
Oncelres = const(0x23)
Changemth= const(0x40) # Or with Mt.7: 5 AS 2: 0
Changemtl= const(0x60) # or with Mt.4: 0 AS 4: 0
Wait=[180,180,0,24]
The declaration of the BH1750 class follows the determination of a few constants, in advance the device address and then the codes for different commands. We can from the Data sheet of the BH1750 remove.
Figure 5: Command overview
The method init() places the constructor BH1750() of the class BH1750 dar. As arguments, when calling in the position parameter I2C The I2C object, and optionally in the keyword parameters ADR, mode and fak Transfer further data to the routine. If the parameters are not listed on the call, then take on the default values.
XXXXXXXXXX
def __init__(self, I2C,
ADR=Hwad,
mode=Conthres,
fak=0.81):
self.I2C = I2C
self.hwad = ADR
self.poweron()
self.fashion(mode)
self._latency=69
self._factor=1
self.factor=fak
print("BH1750 Luxmeter is @",hex(ADR))
So that the values are also available in other methods of the class, we assign them to instance attributes. All objects belonging to an instance are made by a preceding self defined and referenced. So the instance attributes are I2C, hwad, latency and factor. With poweron() and fashion() If defined methods are called up below, referenced. The print command tells us that the object has been created and tells us the hexadecimal representation of the hardware address of the BH1750.
The BH1750 is particularly easy to pick up measured values because the chip always sends data, the combination of two bytes. So we only need to define one method that reads two bytes from the I2C bus. The MSB, the higher -quality byte, comes first.
XXXXXXXXXX
def reap(self):
buf=bytearar(2)
self.I2C.readfrom_into(self.hwad,buf)
return buf
The ESP8266 receives the two bytes and packs them for further processing into an object that follows the buffer protocol. That can be a bytes-be object, or like here, that Bytearar Buf. The function I2C.Readfrom_into() First sends the hardware address and then shovels the received bytes into the buffer. The plot in Figure 6, which I recorded with my Logic Analyzer and the Logic2 program from Saleae, shows what this looks like when transmission on the lines.
Figure 6: Read data from the BH1750
The transmission begins with a start condition, SCL = 1, sda goes to 0. The hardware address is always given with 7 bits, the eighth bit is attached to the right to the address, the Write-Read bit, here it is a 1 . This tells the BH1750 that he should send data. When sending a command to the chip, this bit 0. From 0x23 is 0x46.
With every rising clock flank, the controller samps the SDA line, which means that he looks at the level on the line. After eight clock flanks, the BH1750 pulls the SDA line to 0. With this Acknowledge-bit (ACK-BIT), he indicates that he recognized the address as his. The ESP8266 now knows that it has to prepare for the receipt of the data. With every falling clock flank, the BH1750 now places a database on the SDA line that the controller samps with the rising flank. If the reception has worked without errors, the ESP8266 now sends the ACK bit. The BH1750 then puts the second byte on the way that the ESP8266 acknowledged with a nack bit, not Acknowledge. The controller ends the transmission with a stop condition.
With the method command() we send a command to the BH1750, which we in CMD hand over as an integer. We just stored the values as constants.
XXXXXXXXXX
def command(self,CMD):
buf=bytearar(1)
buf[0]=CMD
self.I2C.writeto(self.hwad,buf)
Now Micropython cannot send this value, which is actually a 32-bitz number as an integer value. Firstly, the BH1750 would not know about the four bytes and secondly, Micropython refuses to send objects such as integers of floating point, lists and other higher objects via the bus. For this purpose, the Softi2c class would have to provide routines for the automatic type conversion in buffer protocol-compliant objects. So let's stuff the command byte into the cell of a bytearar, so that Micropython can do something.
XXXXXXXXXX
>>> I2C=Softi2c(Pin(5),Pin(4),freq=100000)
The send one Integer beats misconception.
>>> I2C.writeto(0x23,0x01)
Traceback (custody recent call load):
File "" , line 1, in <modules>
Type: object wither buffer protocol required
A Bytearar becomes accepted.
>>> buf=bytearar(1)
>>> buf[0]=0x01
>>> I2C.writeto(0x23,buf)
1
As well a bytes-object.
>>> I2C.writeto(0x23,B '\ X01')
1
Or a String
>>> I2C.writeto(0x23,"\ x01")
1
What is said here applies in the same way for the SPI bus, the serial uart interface and the WLAN interface.
What we have just sent to the BH1750 is the power-on command. The method poweron() does the same by that PwronCommand to the method command() is handed over. Works analogously powerdown().
XXXXXXXXXX
def poweron(self):
self.command(Pwron)
XXXXXXXXXX
def powerdown(self):
self.command(Pwrdown)
Figure 7 shows an overview of the conditions of the BH1750 and the transitions from one to the other.
Figure 7: The states of the BH1750
The BH1750 knows various measurement methods. Basically there is a single shot mode and a permanent run mode. Each mode has three different levels of the Lowres resolution, Highthres and Highres2, accordingly 4 lux, 1 lux and 0.5 lux. With the method fashion() we can set the corresponding mode. In order not to do anything wrong, we first check whether the value handed over is included in the list of permissible commands. If this is not the case, an assermentation terror is reported.
XXXXXXXXXX
def fashion(self, mode= None):
IF mode is need None:
assert mode in [
Conthres,
Conthres2,
Contlres,
Oncehres,
Oncehres2,
Oncelres]
self.mode=mode
self.command(mode)
Else:
return self.mode
Because we cannot query the current mode of the BH1750, we remember the value in the instance attribute mode and then send the command. If the parameter is left out when the call, the method provides the value of the instance attribute mode back.
XXXXXXXXXX
>>> bra.fashion(0x81)
Traceback (custody recent call load):
File "" , line 1, in <modules>
File "" , line 57, in fashion
Assertion:
Before the reset command can be sent, the BH1750 must be in the power on state. The note is on page 5 of the Data sheets. By commanding, only the registers with the last measured value are set to 0.
XXXXXXXXXX
def reset(self): # p 5
self.poweron()
self.command(Reset)
With start() we add a single measurement. We can do the desired mode in the parameter fashion hand over. The default value is presented None. If the call is not an argument for mode handed over, then the value of the instance attribute mode used to start the measurement. The measurement duration will list Wait taken. We get the index to the list value from the two lowest quality bits in the commandobytes, i.e. 0, 1 or 3. The index 2 cannot occur. In order for the assignment of the list elements to the modes, we need a third element that I simply set to 0.
XXXXXXXXXX
def start(self, mode=None):
IF mode is need None:
self.fashion(mode)
index= self.mode & 0x03
IF self.mode & 0x20:
self.command(self.mode)
waiting time=max.(BH1750.Wait[index],
BH1750.Wait[index] * self.factor)
Sleep_ms(waiting time)
Else:
raise Value("Wrong mode value")
The setting of a single shot mode can only be made if Bit 5 is set in the command-byte. The IF query ensures this. If it is not set, we will throw a valueeror exception. The main program must intercept this, otherwise the program will be canceled.
If everything has fit, the mode is set and the individual measurement is triggered. The list provides the minimum waiting time until the result is provided Wait, 180 ms for Highres modes and 24 ms for lowres mode.
Now you can adjust the measurement time within certain limits. This becomes necessary if the sensor is covered with a protective glass. Despite the transparency, each glass absorbs part of the light energy, which must be compensated for by an extension of the measuring time. This extension factor must of course be taken into account when calculating the waiting time. For safety's sake, we always take the larger of the two values. Sleep_ms() Then send the ESP8266 briefly into the dreamland.
Regardless of whether it is a single shot, or automatic measurement, the Lux values are used by the method Luminance picked up and prepared.
XXXXXXXXXX
def luminance(self):
Hb,LB=self.reap()
divider= 2 IF self.mode & 0x03 == 1 Else 1
lux=intimately(((Hb << 8) | LB) / (1.2 * (69/self._latency)) / divider) # P11
return lux
The bytearar, that reap() delivers, we unzip into the local variables right away Hb and LB. When the mode Highres2 was active, an additional divider 2 is necessary, says the data sheet on page 11 below. We solve this through a conditional expression. The formulas for calculating the illuminance on page 11 are mathematically wrong, brackets are missing.
Illuminance by 1 Count (LX / COUNT) = 1 / 1.2 *(69 / x) must be called:
Illuminance by 1 Count (LX / COUNT) = 1 / (1.2 *(69 / x)), because with a longer exposure time, a higher value for the luminance must also come out.
We push the MSB to the left by eight positions and ornament With the LSB. This creates a 16-bit data word from two data bytes. This is due to the product from 1.2 and the return value of the correction factor and through divider to divide. The illumination strength comes out in Lux.
In order to change the measurement time, we have already discussed this briefly above, there are two commands. The higher -quality bits tell the BH1750 what to do with the rest.
Figure 8: This changes the measurement duration
The routine period() takes a value from the interval [31… 254] and places it in the instance attribute _latency from that we in the method luminance() need. Then the correction factor of the measurement time is calculated. The following two lines prepare the handed over so that the two commandobytes arise from it. In the data sheet you ask yourself at this point (Figure 8) how that is meant. On page 11 it becomes clearer. The background to the procedure is probably that each command should only consist of one byte. In addition to the payload, the correction value, a key must also be transmitted that tells the BH1750 what to do with the payload. Ultimately, a payload byte then turns into two command-bytes.
XXXXXXXXXX
def period(self,vala=None): # p 11 measurement time compensation
IF vala is need None:
assert vala in range(31,255)
self._latency=vala
self._factor=vala / 69 # 69 is default
Hb=((vala & 0xe0) >> 5 ) | Changemth
LB=(vala & 0x1f ) | Changemtl
self.command(Hb)
self.command(LB)
Else:
return self._latency
So we push the upper three bits of the date, which we through Fierce with 0xe0 = 0b11100000, around five positions to the right and ornament them on the key part Changemth = 0B01000000. We win the lower five bits out with 0x1f = 0b00011111 and or or on Changemtl = 01100000. Then the commands are transferred to the BH1750. Becomes period() called without an argument, the method returns the current value of the correction bytot.
Would you like a little mysticism at the end of the program? - We made a measurement without sensor cover and received the value 587 lux as the result. With a glass cover we measure 391 lux. In order to get back to 587 lux, we have to extend the measurement time with a factor of 1.5 = 587/391. What kind of correction byte does it include? It would be nice if there was a method for conversion. Here you come together with a little micropython mysticism.
XXXXXXXXXX
def factor(self):
return self._factor
setter .
def factor(self,vala): # use: factor = value
assert 0.45 <= vala <= 3.68
self._factor = vala
self.period(intimately(vala*69+0.5)) # sets the measurement time factor
print(self._latency)
The method is used to query the correction factor factor(). @property Is a so -called Decorator. It makes the query in the form possible, as is common with variables, you only indicate the name, the brackets fall away. So be the instance attribute _factor from the value 1.5 then delivers factor just this value. Without the decorator you would have to write factor(). @Property therefore makes the return value of a function as referenced as a simple variable.
Why do we do that? We could just be the instance attribute _factor Query directly, for example.
XXXXXXXXXX
>>> print(bra._factor)
Two reasons speak against it. First, in object -oriented programming, it is part of the fine way not to refer to variables directly. You don't ask directly and you do not assign values directly. Micropython has not installed this access protection, but we can force it. It is not necessary that the user of a program knows the identifiers of attributes and variables. He should only work with the values behind it.
Second, a user/programmer should not be able to assign any values that may be able to crash the program. In fashion() and period() we have with the assert-instruction ensures security. By compiling our module, we can turn the script into a non -readable file that is as executable as the script. Which methods and attributes hide behind it is then no longer visible. We only reveal what we want to release to the user/programmer. By working with the Decorator @Property, we can hide the true name of the variables to be called up. Even more, it could be that further steps have to be taken to call up, which, for example, should change the appearance.
The situation is similar if we know the factor and want to calculate the correction byte from it. We also use a decorator for this, @factor.setter. He also hides the method underneath so that it can be treated like a variable. Of course we could attribute _factor occupy directly. But who then ensures that the value is in the permissible area?
In vala Let us hand over the desired correction factor, which is immediately checked for valid values. The instance attribute _factor If we are updated, then we call the method period() with the calculated correction byte that does the rest. The value of _latency Let's be returned. If the method would not have a decorator, we would have to write Factor (1.5), so we write like a variable Factor = 1.5 And still let the check go through and calculate another value.
What I am here factor have also shown, by the way, could also be included fashion, luminance and period make. Feel free to try it out!
Demo and test
A simple one Demo program shows the use of the module bh1750.py.
XXXXXXXXXX
# bh1750_test.py
From machine import Pin,Softi2c
From BH1750 import BH1750
From time import Sleep_ms
From OLED import OLED
I2C=Softi2c(Pin(5),Pin(4),freq=100000)
bra=BH1750(I2C)
D=OLED(I2C,Heightw=32)
while 1:
D.clearall(False)
D.writer("Luxmeter V1.0",0,0, False)
D.writer("Factor: {: 02f}".format(bra.factor),0,1, False)
D.writer("Lumi: {} lux".format(bra.luminance()),0,2)
The I2C object I2C is used twice, we hand it over to the constructors of the BH1750 and OLED class. In the endless loop, we delete the display, have the heading out, followed by the current correction factor and the luminance value. The argument False leads to the fact that the data is only written in the display buffer. In the last line, this argument is missing, which causes the entire data buffer to be transferred to the display. This saves computing time and suppresses the display.
A Second programs helps to determine the correct correction factor by trial and error.
XXXXXXXXXX
# bh1750_kal.py
From machine import Pin,Softi2c
From BH1750 import BH1750
From time import Sleep_ms
From OLED import OLED
From sys import exit
I2C=Softi2c(Pin(5),Pin(4),freq=100000)
bra=BH1750(I2C)
D=OLED(I2C,Heightw=32)
while 1:
D.clearall(False)
D.writer("Luxmeter V1.0",0,0, False)
D.writer("Factor: {: 02f}".format(bra.factor),0,1, False)
D.writer("Lumi: {} lux".format(bra.luminance()),0,2)
inp=input("Factor (E: Exit; S: Save):")
IF inp=="E":
exit()
elif inp=="S":
wither open("bh1750_kal.txt","W") AS file:
file.write(Str(F)+"\ n")
D.writer("saved",0,2)
exit()
F=float(inp)
bra.factor=F
Sleep_ms(200)
After displaying the values, a value for the factor is required in Repl. After entering, we convert the string into a flow of flow and call the setter factor On - short break - next round. With a luxmeter next to our sensor, we now capture the brightness value and change the factor until both devices indicate roughly the same value. With the "S" button we can save the value and with "E" the loop is left.
A little solder at the end
Now we can record gorgeous measured values, but we cannot connect an actuator. One way out would be the use of a PCF8574 or MCP23017 type, both of which are controlled via the I2C bus.
I chose a different way to bust up the AZ-Eoneboard. I briefly heated up the soldering iron and on the board the unused and also not removed GPIO connections provided with short wire pieces and slices. The GPIOS 0, 2, 15 and 13, 12 and 14 are available to me. If necessary, you could also expand the I2C bus by also using wires to pins 4 and 5.
Figure 9: port expansion
Figure 10: Pin occupancy ESP8266 12F
Figure 11: AZ-Eoneboard in the extended test
In the next episode, we will deal with the SHT30 on the AZ-EONboard and write a module for it. See you then!
2 commentaires
Jürgen
I had seldom problems with boot.py, but You are right, using main.py is the better solution. Thanks for Your contribution.
Jürgen
Frank Carius
Please edit the section about “Autostart”. You should never use the boot.py, because it may block any future usage. I prefer main.py with a delay (time.Sleep(5)) at the beginning to allow a break. See
https://www.msxfaq.de/sonst/bastelbude/esp8266/micropython.htm#boot_py_und_main_py