MicroPython Driver for AHT20 / AHT21
Contents
Introduction
This a MicroPython driver written specifically for the BBC micro:bit that will work with the AHT20 and AHT21 relative humidity and ambient temperature sensors. The driver implements all functions described in the product datasheet.
The AHT2X sensor series is discussed in some detail here. The AHT20 and AHT21 chips are functionally identical with only minor packaging differences.
The AHT20 and AHT21 are readily available on inexpensive breakout boards. There are also breakout boards that feature both the AHT20 and BMP280 humidity and temperature sensors. This is possible because each sensor has a different I2C address.
Fig 1 - AHT20 breakout board: [L to R] front side with dimensions and rear side
Connecting the AHT20 and AHT21 Boards
These sensors communicate via I2C. They must be connected to do the micro:bit as follows:
micro:bit | AHT10 /AHT15 |
---|---|
3.3V | VIN / VDD |
GND | GND |
Pin 19 | SCL |
Pin 20 | SDA |
This utilises the standard I2C pins of the micro:bit.
Driver Overview
The 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.py - OR -
- Download as a zip file using the link. Unzip the file and save it as fc_aht20.py into the default directory where the MicroPython editor e.g. Mu Editor saves python code files.
After saving the fc_aht20.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.
Class constructorThe driver is implemented as a class. The first thing to do is call the constructor. This provides an instance of the class.
Syntax:
instance = AHT20()
Note: There is no option with these sensors
to change the I2C address. Thus the
constructor has no parameters.
Example
from fc_aht20 import *
my_sensor = AHT20()
This assumes that the file fc_aht20.py has been successfully copied to the micro:bit's filesystem as described above.
Class MethodsThe driver provides methods that:
- Initialise() : Initialises the sensor.
- Read_Status() : Returns the Status byte.
- Is_Calibrated() : Checks whether the sensor is calibrated.
- Read(), T(), RH() : Reads the relative humidity and ambient temperature.
- Is_Checksum() : Calculates checksum and compares it with the checksum read from the sensor.
- Reset() : Resets the sensor.
Initialising and Checking Calibration
The datasheet states that the sensor's calibration state should be checked before requesting a read of humidity and temperature. The sensor can be forced to perform it's internal calibration process with an initialisation command. The user can issue an initialisation command manually but in any case this is always done by the constructor.
The constructor also reads the chip's calibration state and the user can check this at any time. The calibration state is defined within the sensor's Status byte. The driver provides a method for the user to return the status but this ordinarily won't be needed. For details of the Status byte the user is referred to the product's datasheet.
Syntax:
Initialise()
Forces the sensor to initialise itself including
running its internal calibration process.
Is_Calibrated()
Returns (True or False) the calibration
status of the sensor.
Read_Status()
Returns the sensor's current Status word.
Example:
from fc_aht20 import *
my_sensor = AHT20()
# Initialise the sensor
my_sensor.Initialise()
# Check calibration state
print('Sensor is calibrated?', my_sensor.Is_Calibrated())
# Get the Status word
print('Status word is:', hex(my_sensor.Read_Status()))
Output:
Sensor is calibrated? True
Status word is: 0x18
Reading Temperature and Humidity
There are three methods that return the humidity and/or temperature.
Additionally the sensor calculates an 8-bit CRC checksum across the 40-bits of combined humidity and temperature values that can be read from the sensor. This driver has an internal method that calculates a checksum from the data when it is received and compares it with the sensor's checksum. The user can ask for the result of this checksum comparison.
- Read()
- RH()
- T()
- Is_Checksum()
Read()
Returns a tuple containing three values:
[0] is relative humidity in %,
[1] is temperature in °C
[2] is the result of the checksum comparison; True or False.
True indicates that the humidity and temperature
readings can be regarded with high confidence.
Humidity and temperature are of type float and
reported at maximum MicroPython resolution.
RH()
Returns relative humidity in °%
The result is returned as an integer.
T()
Returns temperature in °C
The result is rounded to one decimal.
Is_Checksum()
Returns the result of the checksum comparison for the
latest humidity and temperature reading; True or False.
Example:
from fc_aht20 import *
from microbit import sleep
aht20 = AHT20()
# Read humidity and temperature
print('Read results:', aht20.Read())
sleep(1000)
# Get temperature only
print('Temperature(C):', aht20.T())
sleep(1000)
# Get relative humidity only
print('RH(%):', aht20.RH())
# Get checksum comparison
print('Data is good?', aht20.Is_Checksum())
Sample Output:
Read results: (43.48345, 23.51894, True)
Temperature(C): 23.5
RH(%): 43
Data is good? True
NOTE: The datasheet advises that no more than one reading should be made per second. This is to ensure that the internal temperature of the sensor doesn't increase by more than 0.1°C above the atmospheric ambient temperature.
Resetting the Sensor
The user can send a soft reset command to the sensor chip. This can be done by calling the Reset() method. The reset restores default settings and is completed in less than 20ms.
from fc_aht20 import *
my_sensor = AHT20()
my_sensor.Reset()
Examples
Example 1Three different humidity and temperature sensors, AHT10, AHT20 and SHT40, will be used for both examples. Each sensor will be read in sequence for humidity and temperature each 1 second for 5 seconds i.e. a total of 5 readings from each sensor.
Each 1 second reading will be output. Then the average humidity and temperature readings from each sensor will be calculated and output.
Since the AHT20 and AHT21 sensors have the same I2C address they will be attached to channels on a TCA9548A I2C multiplexer. The AHT10 will use channel 7 and the AHT20 will use channel 2 of the multiplexer.
The following driver module files need to be copied to the micro:bit's filesystem:
- AHT20 : fc_aht20.py
- TCA9548A : fc_tca9548a.py
- SHT40 : fc_sht4x.py
- Connect TCA9548A, AHT20, AHT21 and SHT40 power and ground pins to the micro:bit's 3.3V and GND pins respectively.
- TCA9548A: Connect SCL and SDA to micro:bit's pin19 and pin20 respectively.
- SHT40: Connect SCL and SDA to micro:bit's pin19 and pin20 respectively.
- AHT20: Connect SCL and SDA pins to TCA9548A's SC7 and SD7 pins respectively.
- AHT21: Connect SCL and SDA pins to TCA9548A's SC2 and SD2 pins respectively.
Example 1
# Simultaneously test AHT20, AHT21
# and SHT40 sensors.
# All three sensors measure humidity
# and temperature.
# AHT20 and AHT21 sensors have the
# same I2C address so it is necessary
# to use a TCA9548A multiplexer.
from fc_aht20 import *
from fc_tca9548a import *
from fc_sht4x import *
from microbit import sleep
# Instantiate multiplexer object.
multi = TCA9548A()
# Instantiate SHT40 sensor object.
sht40 = SHT4X()
# Instantiate AH210 sensor object.
# Attached to multiplexer channel 7
multi.CloseAllChannels()
multi.OpenChannels(7)
aht20 = AHT20()
# Instantiate AHT21 sensor object.
# Attached to multiplexer channel 2
multi.CloseAllChannels()
multi.OpenChannels(2)
aht21 = AHT20()
sum_aht20_rh = sum_aht21_rh = sum_sht40_rh = 0
sum_aht20_t = sum_aht21_t = sum_sht40_t = 0
# Read the sensors
for x in range(5):
# AHT20 sensor
multi.CloseAllChannels()
multi.OpenChannels(7)
data = aht20.Read()
sum_aht20_t += data[1]
sum_aht20_rh += data[0]
print('AHT20 - RH(%):', data[0], 'T(C):', data[1])
# AHT21 sensor
multi.CloseAllChannels()
multi.OpenChannels(2)
data = aht21.Read()
sum_aht21_t += data[1]
sum_aht21_rh += data[0]
print('AHT21 - RH(%):', data[0], 'T(C):', data[1])
# SHT40 sensor
data = sht40.Read()
sum_sht40_t += data[0]
sum_sht40_rh += data[2]
print('SHT40 - RH(%):', data[2], 'T(C):', data[0])
sleep(1000)
# Calculate averages
avg_aht20_t = sum_aht20_t / 5
avg_aht20_rh = sum_aht20_rh / 5
avg_aht21_t = sum_aht21_t / 5
avg_aht21_rh = sum_aht21_rh / 5
avg_sht40_t = sum_sht40_t / 5
avg_sht40_rh = sum_sht40_rh / 5
print('Averages:')
print('AHT20 - RH(%):', avg_aht20_rh, 'T(C):', avg_aht20_t)
print('AHT21 - RH(%):', avg_aht21_rh, 'T(C):', avg_aht21_t)
print('SHT40 - RH(%):', avg_sht40_rh, 'T(C):', avg_sht40_t)
Typical Output:
AHT20 - RH(%): 38.82132 T(C): 26.84288
AHT21 - RH(%): 39.84413 T(C): 27.11792
SHT40 - RH(%): 41.89807 T(C): 26.69833
AHT20 - RH(%): 38.81693 T(C): 26.82991
AHT21 - RH(%): 39.79225 T(C): 27.09312
SHT40 - RH(%): 41.94576 T(C): 26.701
AHT20 - RH(%): 38.82599 T(C): 26.83163
AHT21 - RH(%): 39.76469 T(C): 27.09827
SHT40 - RH(%): 41.90952 T(C): 26.70901
AHT20 - RH(%): 38.82656 T(C): 26.83697
AHT21 - RH(%): 39.91823 T(C): 27.0937
SHT40 - RH(%): 41.8542 T(C): 26.69833
AHT20 - RH(%): 38.82713 T(C): 26.82571
AHT21 - RH(%): 39.83307 T(C): 27.08626
SHT40 - RH(%): 41.87519 T(C): 26.68765
Averages:
AHT20 - RH(%): 38.82359 T(C): 26.83342
AHT21 - RH(%): 39.83047 T(C): 27.09785
SHT40 - RH(%): 41.89655 T(C): 26.69886
Example 2
The first example code was slightly modified to take readings from the three sensors sequentially every 1 seconds for (slightly more than) one hour i.e. 3,600 readings were taken by each sensor. The code modifications made were:
- The range in the for loop was changed from 5 to 3600.
- The three print statements inside the loop were commented out.
- The average calculations were changed to reflect that 3,600 samples per sensor were taken.
Typical Output:
Averages:
AHT20 - RH(%): 57.97791 T(C): 19.09774
AHT21 - RH(%): 57.59734 T(C): 19.08545
SHT40 - RH(%): 56.52404 T(C): 19.59215
All three sensors showed reasonably close results for relative humidity which is the primary function of these sensors.
The SHT40, which would be regarded as the best sensor of the three, displayed a slightly higher temperature reading. This was also observed when this test was performed with the SHT40, AHT10 and AHT15 trio.
Enjoy!
AHT20, AHT21 Driver Code for micro:bit
Download as zip file
'''
AHT20, AHT21 (humidity and temperature sensors)
MicroPython driver for micro:bit
CREDIT for CRC calculation:
SOURCE: AHT30 Datasheet, 3.CRCcheck, page 11.
Function: unsigned char Calc_CRC8()
Translated from C++
to MicroPython for the micro:bit.
AUTHOR: fredscave.com
DATE : 2024/11
VERSION : 1.00
'''
from microbit import i2c, sleep
from micropython import const
ADDR = const(0x38)
CMD_INIT = [0xBE, 0x08, 0x00]
CMD_MEASURE = [0xAC, 0x33, 0x00]
CMD_RESET = const(0xBA)
CRC_INIT = const(0xFF)
CRC_POLY = const(0x31)
class AHT20():
def __init__(self, addr=ADDR):
sleep(100)
self.addr = addr
self.IsChecksum = False
self.Initialise()
sleep(10)
self.IsCalibrated = self.Read_Status() & 0b00001000
def Initialise(self):
i2c.write(self.addr, bytearray(CMD_INIT))
def Read_Status(self):
buf = i2c.read(self.addr, 1)
return buf[0]
def Is_Calibrated(self):
return bool(self.IsCalibrated)
def Is_Checksum(self):
return self.IsChecksum
def Read(self):
i2c.write(self.addr, bytearray(CMD_MEASURE))
sleep(80)
busy = True
while busy:
#buf = i2c.read(self.addr, 1)
sleep(10)
busy = self.Read_Status() & 0b10000000
buf = i2c.read(self.addr, 7)
measurements = self._Convert(buf)
return measurements
def T(self):
measurements = self.Read()
return round(measurements[1], 1)
def RH(self):
measurements = self.Read()
return int(measurements[0] + 0.5)
def Reset(self):
i2c.write(self.addr, bytes([CMD_RESET]))
sleep(20)
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
self.IsChecksum = self._Compare_Checksum(buf)
return (RH, T, self.IsChecksum)
def _Compare_Checksum(self, buf):
check = bytearray(1)
check[0] = CRC_INIT
for byte in buf[:6]:
check[0] ^= byte
for x in range(8):
if check[0] & 0b10000000:
check[0] = (check[0] << 1) ^ CRC_POLY
else:
check[0] = check[0]<< 1
return check[0] == buf[6]