MicroPython Driver for AHT20 & BMP280 Breakout Board
Contents
Introduction
This a MicroPython driver written specifically for the BBC micro:bit that will work with the AHT20 & BMP280 breakout board.
This breakout board, as the name suggests, contains one AHT20 sensor chip and one BMP280 sensor chip. The only other components on the board are two 4.7 kΩ pullup resistors on the I2C pins and a decoupling capacitor on the power supply.
The AHT20 sensor measures temperature and relative humidity and is discussed in some detail here.
The BMP280 sensor measures temperature and barometric pressure and is discussed in some detail here.
Connecting the Breakout Board
This board communicates with a microcontroller via I2C. Hooking it up to the micro:bit for I2C is easy:
| micro:bit | Breakout board |
|---|---|
| 3.3V | VDD |
| GND | GND |
| Pin 20 | SDA |
| Pin 19 | SCL |
This utilises the standard I2C pins of the micro:bit.
Driver Overview
This is a minimalist driver. It only provides functionality to read temperature & relative humidity from the ATH20 and temperature & barometric pressure from the the BMP280.
Driver codeThe driver code can be:
- Copied from this webpage onto the clipboard then pasted into the MicroPython editor e.g. the Mu Editor. It should be saved as fc_aht20_bmp280.py - OR -
- Download as a zip file using the link. Unzip the file and save it as fc_aht20_bmp280.py into the default directory where the MicroPython editor e.g. Mu Editor saves python code files.
After saving the fc_aht20_bmp280.py file to the computer it should be copied to the small filesystem on the micro:bit. The examples on this page will not work if this step is omitted. Any MicroPython editor that recognises the micro:bit will have an option to do this.
The Mu Editor is recommended for its simplicity. It provides a Files button on the toolbar for just this purpose.
I2C addressesSince there are two sensors connected via I2C there are two addresses. We performed an I2C bus scan on the board we purchased for the development of this driver and found the following two addresses:
- AHT20: 0x38
- BMP280: 0x77
These two addresses are coded as constants at the head of the driver code file.
The address of the BMP280 was a bit of a surprise as we expected it to be (the more common) 0x76. If in doubt, the advice is to run an I2C scan. The following code can be used:
Code
# Performs a scan of the I2C bus.
# Reports the number of devices
# on the bus and their addresses.
# I2C with defaults:
# freq = 100000
# sda = pin20
# scl = pin19
from microbit import i2c, pin19, pin20
# Perform scan of I2C bus.
addr = i2c.scan()
# Get the number of devices found.
num = len(addr)
# Report number of device found
# along with their I2C addresses.
print('I2C Devices:', num)
print('Addresses:')
for i in range(num):
print(hex(addr[i]))
Output:
I2C Devices: 2
Addresses:
0x38
0x77
Class constructor
The driver is implemented as a class. The first thing to do is call the constructor. This provides an instance of the class.
Syntax:
SENSORS(H=None)
Where:
H : This is the height of the sensor above
sea level in metres. It is used to
calculate MSLP (Mean Sea Level Pressure).
If not supplied this calculation can't be done.
Example
from fc_aht20_bmp280 import *
# Create a sensor object.
# It is known that board is
# 400 metres above sea level.
sensor = SENSORS(H=400)
This assumes that the file fc_aht20_bmp280.py has been successfully copied to the micro:bit's filesystem as described above.
MethodsThis driver only provides one public method; Read()
Reading Temperature, Pressure and Humidity
Syntax:
Read()
Returns temperature, pressure and humidity as elements
of a list. The five elements of the list are:
[0] : BMP280 temperature
[1] : BMP280 barometric pressure (absolute)
[2] : Calculated mean sea level pressure (MSLP)
[3] : AHT20 temperature
[4] : AHT20 relative humidity
Mean sea level pressure is the absolute pressure
from the BMP280 that is adjusted to the equivalent
sea level pressure. This is the value that is
reported by all official weather services.
Temperature is in degrees Celsius.
Pressure is in hPa (hectopascals).
Humidity is in %RH.
Example:
# Tests the dual sensor breakout board
# with AHT20 and BMP280 sensors.
from fc_aht20_bmp280 import *
# Declare a sensor board that
# is 400m above sea level
sensor = SENSORS(400)
# Read the AHT20 and BMP280 sensors
Buf = sensor.Read()
# Report the results
print('Values returned by Read() method')
print(Buf)
print('\nBMP280')
print('T:', Buf[0], ' P:', Buf[1], ' MSLP:', Buf[2])
print('\nAHT20')
print('T:', Buf[4], ' RH:', Buf[3])
Typical Output:
Values returned by Read() method
[23.51, 984.34, 1031.479, 45.64829, 22.61791]
BMP280
T: 23.51 P: 984.34 MSLP: 1031.479
AHT20
T: 22.61791 RH: 45.64829
Enjoy!
AHT20 & BMP280 Breakout Board Driver Code for micro:bit
Download as zip file
'''
This module supplies driver support for the
AHT20 & BMP280 breakout board. The drivers
are written specifically for the BBC micro:bit.
This Module contains MicroPython drivers for:
- AHT20 Sensor (temperature & humidity)
- BMP280 Sensor (temperature and pressure)
Temperature : Celsius.
Pressure : hPa.
Relative Humidity : %
USAGE EXAMPLE:
sensor = SENSORS(400)
Buf = sensor.Read()
USAGE NOTES:
(1) In this case the Breakout board
is 400m above sea level. This is
used for the MSLP calculation.
(2) BMP280
Buf[0]: Temperature, Buf[1]: Pressure
Buf[2]: Mean Sea Level Pressure (MSLP)
(3) AHT20
Buf[3]: Humidity, Buf[4]: Temperature
(4) I2C Addresses
BMP280: 0x77, AHT20: 0x28
AUTHOR: fredscave.com
DATE : 2025/05
VERSION : 1.00
'''
AHT20 = 0x38
BMP280 = 0x77
from microbit import *
class SENSORS():
def __init__(self, H=None):
# Initialise BMP280
self._Load_Calibration_Data()
self.H = H
self._writeReg(BMP280, [0xF5, 0x08])
# Initialise AHT20
sleep(100)
self._writeReg(AHT20, [0xBE, 0x08, 0x00])
sleep(10)
# Read from AHT20 and BMP280
def Read(self):
aht20 = self._ReadAHT20()
bmp280 = self._ReadBMP280()
mslp = self._MSLP(bmp280[1])
return [bmp280[0], bmp280[1], mslp,
aht20[0], aht20[1]]
def _ReadAHT20(self):
self._writeReg(AHT20, [0xAC, 0x33, 0x00])
sleep(80)
busy = True
while busy:
sleep(10)
buf = i2c.read(AHT20, 1)
busy = buf[0] & 0b10000000
buf = i2c.read(AHT20, 7)
measurements = self._Convert(buf)
return measurements
def _Convert(self, buf):
RawRH = ((buf[1] << 16) |( buf[2] << 8) | buf[3]) >> 4
RH = RawRH * 100 / 0x100000
RawT = ((buf[3] & 0x0F) << 16) | (buf[4] << 8) | buf[5]
T = ((RawT * 200) / 0x100000) - 50
return (RH, T)
def _ReadBMP280(self):
# Read uncompensated temperature and pressure.
self._writeReg(BMP280, [0xF4, 0x2D])
sleep(15)
buf = self._readReg(BMP280, 0xF7, 6)
adc_P = (buf[0] << 12) + (buf[1] << 4) + (buf[2] >> 4)
adc_T = (buf[3] << 12) + (buf[4] << 4) + (buf[5] >> 4)
var1 = (((adc_T>>3)-(self.dig_T1<<1))*self.dig_T2)>>11
# Calculated actual temperature.
var2 = (((((adc_T>>4)-self.dig_T1)*((adc_T>>4) - self.dig_T1))>>12)*self.dig_T3)>>14
t_fine = var1 + var2
T = ((t_fine * 5 + 128) >> 8)/100
# Calculate actual pressure
var1 = (t_fine >> 1) - 64000
var2 = (((var1 >> 2) * (var1 >> 2)) >> 11 ) * self.dig_P6
var2 = var2 + ((var1 * self.dig_P5) <<1)
var2 = (var2 >> 2)+(self.dig_P4 << 16)
var1 = (((self.dig_P3*((var1>>2)*(var1>>2))>>13)>>3) + (((self.dig_P2) * var1)>>1))>>18
var1 = ((32768 + var1) * self.dig_P1) >> 15
if var1 == 0:
return # avoid exception caused by division by zero
p = ((1048576 - adc_P) - (var2 >> 12)) * 3125
if p < 0x80000000:
p = (p << 1) // var1
else:
p = (p // var1) * 2
var1 = (self.dig_P9 * (((p >> 3) * (p >> 3)) >> 13)) >> 12
var2 = (((p >> 2)) * self.dig_P8) >> 13
P = p + ((var1 + var2 + self.dig_P7) >> 4)
return [T, P/100]
# Calculate mean sea level pressure (MSLP)
def _MSLP(self, P):
if self.H == None:
return None
else:
P0 = 1013.25
a = 2.25577E-5
b = 5.25588
PS = P0 * (1 - a * self.H) ** b
offset = P0 - PS
return P + offset
# Get the BMP280 trimming constants from NVM.
def _Load_Calibration_Data(self):
self.dig_T1 = self._getCal(0x88, 0)
self.dig_T2 = self._getCal(0x8A, 1)
self.dig_T3 = self._getCal(0x8C, 1)
self.dig_P1 = self._getCal(0x8E, 0)
self.dig_P2 = self._getCal(0x90, 1)
self.dig_P3 = self._getCal(0x92, 1)
self.dig_P4 = self._getCal(0x94, 1)
self.dig_P5 = self._getCal(0x96, 1)
self.dig_P6 = self._getCal(0x98, 1)
self.dig_P7 = self._getCal(0x9A, 1)
self.dig_P8 = self._getCal(0x9C, 1)
self.dig_P9 = self._getCal(0x9E, 1)
# Writes one or more bytes to register.
# Bytes is expected to be a list.
# First element is the register address.
def _writeReg(self, ADDR, Bytes):
i2c.write(ADDR, bytes(Bytes))
# Read a given number of bytes from
# a register.
def _readReg(self, ADDR, Reg, Num):
self._writeReg(ADDR, [Reg])
buf = i2c.read(ADDR, Num)
return buf
# Does the grunt work retrieving the
# trimming constants from the BMP280's
# non-volatile memory (NVM).
def _getCal(self, Reg, Signed):
buf = self._readReg(BMP280, Reg, 2)
d = buf[1]*256 + buf[0]
if Signed == 1:
if d > 32767:
return d - 65536
else:
return d
else:
return d