MicroPython Driver for AHT30 Sensor
Contents
Introduction
This a MicroPython driver written specifically for the BBC micro:bit that will work with the AHT30 relative humidity and ambient temperature sensors. The driver implements all functions described in the product datasheet.
The AHT30 sensor is discussed in some detail here.
The AHT30 is readily available on inexpensive breakout boards.
Fig 1 - AHT30 breakout board: [L to R] front side and rear side
Connecting the AHT30 Board
This sensor communicate via I2C. It must be connected to the micro:bit as follows:
micro:bit | AHT30 |
---|---|
3.3V | VIN |
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_aht30.py - OR -
- Download as a zip file using the link. Unzip the file and save it as fc_aht30.py into the default directory where the MicroPython editor e.g. Mu Editor saves python code files.
After saving the fc_aht30.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 = AHT30()
Note: There is no option with these sensors
to change the I2C address. Thus the
constructor has no parameters.
Example
from fc_aht30 import *
my_sensor = AHT30()
This assumes that the file fc_aht30.py has been successfully copied to the micro:bit's filesystem as described above.
Class MethodsThe AHT30 datasheet is very scant on details when it comes to describing what functionality and commands are available to the user through the I2C interface. Section 5 of the datasheet, Sensor communication only discusses the following:
- Status byte : How to read it, then interpret the bits.
- Measurement : Command to measure humidity and temperature, reading the data bytes, calculating and comparing checksums then converting to RH% and °C respectively.
The ability to initialise and soft reset the sensor is not covered at all. However the AHT30 sensor was tested with this AHT20 driver. All user available methods were tried with no 'complaints' from the AHT30. So it appears that the AHT30 is likely to be compatible with its AHT20 predecessor.
The driver provides methods that:
- Read and interpret the Status byte
- Read_Status()
- Busy()
- Mode()
- CRC_Flag()
- Calibrated()
- CMP_Interrupt()
- Return Relative Humidity, Temperature and checksum comparison
- Read()
- RH()
- T()
- Is_Checksum()
Reading and Interpreting the Status Byte
This is how the datasheet describes the bits of the Status byte:
Bit | Significance | Description |
---|---|---|
Bit[7] | Busy Indicator | 0 : Sensor is idle 1: Sensor is busy |
Bit6:5] | Current Mode | 00 : NOR 01 : CYC 1x : CMD |
Bit[4] | CRC Flag | 0 : OTP integrity test failed 1 : OTP integrity test passed |
Bit[3] | Calibration | 0 : Calibration failed 1 : Calibration passed |
Bit[2] | CMP Interrupt | 0 : CMP Interrupt not set 1 : CMP Interrupt is set |
Bit1:0] | Reserved | - - |
Syntax:
Read_Status()
Returns the Status byte.
Busy()
Returns True if the sensor is busy taking
and processing a measurement, otherwise False.
Mode()
Returns the sensor's current working mode.
One of NOR, CYC or CMD.
CRC_Flag()
Returns True if the OTP memory data integrity
test (CRC) passed, otherwise False.
OTP is one-time programmable memory and holds the
calibration constants for the sensor calculations.
Calibrated()
Returns True if sensor successfully completed
its calibration on start-up, otherwise False.
If the calibration failed, measurement data will
still be returned on command but won't be
calibration compensated.
CMP_Interrupt()
Returns True if the calibrated capacitance
data exceeds the CMP interrupt threshold range,
otherwise False.
Example:
# Test the driver for the AHT30 sensor.
# Examine the Status byte.
from fc_aht30 import *
aht30 = AHT30()
# Print information from the
# sensor's Status byte.
print('Status byte:', hex(aht30.Read_Status()))
print('Sensor busy:', aht30.Busy())
print('Sensor mode:', aht30.Mode())
print('OTP memory data OK:', aht30.CRC_Flag())
print('Sensor calibrated:', aht30.Calibrated())
print('CMP interrupt enabled:', aht30.CMP_Interrupt())
Output:
Status byte: 0x18
Sensor busy: False
Sensor mode: NOR
OTP memory data OK: True
Sensor calibrated: True
CMP interrupt enabled: False
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:
# Test the driver for the AHT30 sensor.
# Read the relative humidity and temperature
# using all the different methods.
from fc_aht30 import *
from microbit import sleep
aht30 = AHT30()
# Return RH and temperature at full precision.
data = aht30.Read()
print('RH(%):', data[0], ' T(C):', data[1])
print('Data is good:', data[2])
# Return rounded RH and temperature.
sleep(1000)
t = aht30.T()
sleep(1000)
rh = aht30.RH()
check = aht30.Is_Checksum()
print('\nRH(%):', rh, ' T(C):', t)
print('Data is good:', check)
Sample Output:
RH(%): 48.54212 T(C): 26.01471
Data is good: True
RH(%): 48 T(C): 26.0
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.
Examples
Example 1Three different humidity and temperature sensors, AHT20, AHT30 and SHT40 (from Sensirion), will be used in both examples.
In the first example each sensor will be read in sequence for humidity and temperature at 1 second intervals for 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 AHT30 sensors have the same I2C address they will be attached to channels on a TCA9548A I2C multiplexer. The AHT20 will use channel 7 and the AHT30 will use channel 2 of the multiplexer.
The following driver module files need to be copied to the micro:bit's filesystem:
- AHT30 : fc_aht30.py
- AHT20 : fc_aht20.py
- TCA9548A : fc_tca9548a.py
- SHT40 : fc_sht4x.py
- Connect TCA9548A, AHT20, AHT30 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.
- AHT30: Connect SCL and SDA pins to TCA9548A's SC2 and SD2 pins respectively.
Example 1
# Simultaneously test AHT20, AHT30
# and SHT40 sensors.
# All three sensors measure humidity
# and temperature.
# AHT20 and AHT30 sensors have the
# same I2C address so it is necessary
# to use a TCA9548A multiplexer.
from fc_aht20 import *
from fc_aht30 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 AHT20 sensor object.
# Attached to multiplexer channel 7
multi.CloseAllChannels()
multi.OpenChannels(7)
aht20 = AHT20()
# Instantiate AHT30 sensor object.
# Attached to multiplexer channel 2
multi.CloseAllChannels()
multi.OpenChannels(2)
aht30 = AHT30()
sum_aht20_rh = sum_aht30_rh = sum_sht40_rh = 0
sum_aht20_t = sum_aht30_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])
# AHT30 sensor
multi.CloseAllChannels()
multi.OpenChannels(2)
data = aht30.Read()
sum_aht30_t += data[1]
sum_aht30_rh += data[0]
print('AHT30 - 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_aht30_t = sum_aht30_t / 5
avg_aht30_rh = sum_aht30_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('AHT30 - RH(%):', avg_aht30_rh, 'T(C):', avg_aht30_t)
print('SHT40 - RH(%):', avg_sht40_rh, 'T(C):', avg_sht40_t)
Typical Output:
AHT20 - RH(%): 60.51016 T(C): 22.44473
AHT30 - RH(%): 57.04346 T(C): 22.60303
SHT40 - RH(%): 59.06256 T(C): 22.90379
AHT20 - RH(%): 60.51798 T(C): 22.44549
AHT30 - RH(%): 57.08942 T(C): 22.60838
SHT40 - RH(%): 59.08354 T(C): 22.90379
AHT20 - RH(%): 60.51283 T(C): 22.43862
AHT30 - RH(%): 57.07483 T(C): 22.61677
SHT40 - RH(%): 59.07401 T(C): 22.89044
AHT20 - RH(%): 60.51569 T(C): 22.42908
AHT30 - RH(%): 57.10382 T(C): 22.60971
SHT40 - RH(%): 59.09689 T(C): 22.89311
AHT20 - RH(%): 60.51464 T(C): 22.44339
AHT30 - RH(%): 57.09143 T(C): 22.61105
SHT40 - RH(%): 59.05684 T(C): 22.89311
Averages:
AHT20 - RH(%): 60.51426 T(C): 22.44026
AHT30 - RH(%): 57.08059 T(C): 22.60979
SHT40 - RH(%): 59.07477 T(C): 22.89685
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:
AHT20 - RH(%): 55.73978 T(C): 24.04087
AHT30 - RH(%): 52.51987 T(C): 24.18169
SHT40 - RH(%): 55.34979 T(C): 24.40399
All three sensors showed close results for temperature.
The AHT20 and SHT40 showed very close results for relative humidity. The AHT30's average was about 3% (in absolute terms) lower than the other two sensors. However, this is still (just) within the typical accuracy of the AHT30 so might be considered acceptable.
Enjoy!
AHT30 Driver Code for micro:bit
Download as zip file
'''
AHT30 (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_MEASURE = [0xAC, 0x33, 0x00]
CRC_INIT = const(0xFF)
CRC_POLY = const(0x31)
MASK_BUSY = const(0b10000000)
MASK_MODE = const(0b01100000)
MASK_CRC_FLAG = const(0b00010000)
MASK_CALIBRATION = const(0b00001000)
MASK_CMP_INTERRUPT = const(0b00000100)
class AHT30():
def __init__(self, addr=ADDR):
sleep(10)
self.addr = addr
self.IsChecksum = False
def Read_Status(self):
buf = i2c.read(self.addr, 1)
return buf[0]
def Busy(self):
x = self.Read_Status() & MASK_BUSY
return bool(x)
def Mode(self):
x = self.Read_Status() & MASK_MODE
x = x >> 5
if x == 0b00:
return 'NOR'
elif x == 0b01:
return 'CYC'
else:
return 'CMD'
def CRC_Flag(self):
x = self.Read_Status() & MASK_CRC_FLAG
return bool(x)
def Calibrated(self):
x = self.Read_Status() & MASK_CALIBRATION
return bool(x)
def CMP_Interrupt(self):
x = self.Read_Status() & MASK_CMP_INTERRUPT
return bool(x)
def Is_Checksum(self):
return self.IsChecksum
def Read(self):
i2c.write(self.addr, bytearray(CMD_MEASURE))
sleep(80)
busy = True
while self.Busy():
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 _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]