The AHT20 is a cheap, accurate humidity sensor that talks I2C. MicroPython has no driver for it, but the protocol is tiny: send a trigger command, wait 80 ms, read seven bytes.
Wire it to 3V3 and I2C0 (GP4/GP5). The script reads it every two seconds and checks each reading’s CRC.
Wiring
| Raspberry Pi Pico W | AHT20 temperature + humidity sensor | Wire | Notes |
|---|---|---|---|
| 3V3 | VCC | red | 3V3 is pin 36. The pull-ups follow VCC, so 3.3 V keeps SDA/SCL Pico-safe. |
| GND | GND | black | |
| GP5 | SCL | yellow | GP5 is physical pin 7 (I2C0 SCL). |
| GP4 | SDA | blue | GP4 is physical pin 6 (I2C0 SDA). |
2.0–5.5 V part, 3.3 V board. Most AHT20 breakouts have pull-ups to their own VCC. On ESP32, Pico and other 3.3 V boards, power the module from 3.3 V so SDA/SCL never rise above 3.3 V.
Board quirks
- The AHT20 has a fixed address,
0x38. On the popular AHT20 + BMP280 board the BMP280 answers at 0x77 on the same two wires — see the BMP280 guide to read both. - Every reading carries a CRC byte; the script checks it, so a loose jumper prints “CRC mismatch” instead of a silly number.
- It is a big step up from the DHT11 (±0.3 °C, ±2 % RH) at about the same price, and I2C means no timing-sensitive single-wire protocol.
Code
# AHT20 temperature + humidity on a Raspberry Pi Pico W (MicroPython, no library).
# Wiring: VCC -> 3V3 (pin 36), GND -> GND, SCL -> GP5 (pin 7), SDA -> GP4 (pin 6). I2C0, address 0x38.
# Also works for the AHT20 half of the common AHT20 + BMP280 combo board.
from machine import Pin, I2C
import time
ADDR = 0x38
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=100_000)
if ADDR not in i2c.scan():
raise SystemExit("No AHT20 at 0x38 - found {}".format([hex(a) for a in i2c.scan()]))
time.sleep_ms(40) # power-on time
if not i2c.readfrom(ADDR, 1)[0] & 0x08: # calibration bit not set -> initialise
i2c.writeto(ADDR, b"\xBE\x08\x00")
time.sleep_ms(10)
def crc8(data): # polynomial 0x31, init 0xFF
crc = 0xFF
for b in data:
crc ^= b
for _ in range(8):
crc = ((crc << 1) ^ 0x31) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
return crc
def read():
i2c.writeto(ADDR, b"\xAC\x33\x00") # trigger a measurement
time.sleep_ms(80)
for _ in range(10):
d = i2c.readfrom(ADDR, 7)
if not d[0] & 0x80: # busy bit clear
break
time.sleep_ms(10)
if crc8(d[:6]) != d[6]:
raise OSError("CRC mismatch")
rh = ((d[1] << 12) | (d[2] << 4) | (d[3] >> 4)) * 100 / 1048576
t = (((d[3] & 0x0F) << 16) | (d[4] << 8) | d[5]) * 200 / 1048576 - 50
return t, rh
print("AHT20 at 0x38")
while True:
try:
t, rh = read()
print("Temp {:.1f} C Humidity {:.1f} %".format(t, rh))
except OSError as e:
print("Read failed:", e)
time.sleep(2)# AHT20 temperature + humidity on a Raspberry Pi Pico W (MicroPython, no library).
# Wiring: VCC -> 3V3 (pin 36), GND -> GND, SCL -> GP5 (pin 7), SDA -> GP4 (pin 6). I2C0, address 0x38.
# Also works for the AHT20 half of the common AHT20 + BMP280 combo board.
from machine import Pin, I2C
import time
ADDR = 0x38
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=100_000)
if ADDR not in i2c.scan():
raise SystemExit("No AHT20 at 0x38 - found {}".format([hex(a) for a in i2c.scan()]))
time.sleep_ms(40) # power-on time
if not i2c.readfrom(ADDR, 1)[0] & 0x08: # calibration bit not set -> initialise
i2c.writeto(ADDR, b"\xBE\x08\x00")
time.sleep_ms(10)
def crc8(data): # polynomial 0x31, init 0xFF
crc = 0xFF
for b in data:
crc ^= b
for _ in range(8):
crc = ((crc << 1) ^ 0x31) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
return crc
def read():
i2c.writeto(ADDR, b"\xAC\x33\x00") # trigger a measurement
time.sleep_ms(80)
for _ in range(10):
d = i2c.readfrom(ADDR, 7)
if not d[0] & 0x80: # busy bit clear
break
time.sleep_ms(10)
if crc8(d[:6]) != d[6]:
raise OSError("CRC mismatch")
rh = ((d[1] << 12) | (d[2] << 4) | (d[3] >> 4)) * 100 / 1048576
t = (((d[3] & 0x0F) << 16) | (d[4] << 8) | d[5]) * 200 / 1048576 - 50
return t, rh
print("AHT20 at 0x38")
while True:
try:
t, rh = read()
print("Temp {:.1f} C Humidity {:.1f} %".format(t, rh))
except OSError as e:
print("Read failed:", e)
time.sleep(2)Expected serial output
AHT20 at 0x38
Temp 27.8 C Humidity 61.4 %
Temp 27.8 C Humidity 61.6 %AHT20 at 0x38 Temp 27.8 C Humidity 61.4 % Temp 27.8 C Humidity 61.6 %
Common errors
No AHT20 at 0x38
Check the four wires and 3V3. Run the script once more after a power-cycle — the sensor needs 40 ms after power-up.
Humidity stuck at 0 % or 100 %
The calibration bit was never set. Power-cycle the module so the script sends the initialise command again.