MicroPython Driver for AHT20 & BMP280 Breakout Board

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