Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.

AHT20 with Raspberry Pi Pico W — MicroPython temperature and humidity

Read an AHT20 (or the AHT20 + BMP280 combo board) on Pico W I2C0 (GP4/GP5) in MicroPython with a CRC-checked driver in 30 lines.

MicroPythonVerified — micropython (mpy-cross v1.29.0), 3 Nov 2026Compile-only

Community draft — report a mistake

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 WOnly the pins used are shown · 3.3 V logicAHT20 temperature + humidity sensorGNDGP4GP53V3VCCGNDSCLSDA
Every connection
Raspberry Pi Pico WAHT20 temperature + humidity sensorWireNotes
3V3VCCred3V3 is pin 36. The pull-ups follow VCC, so 3.3 V keeps SDA/SCL Pico-safe.
GNDGNDblack
GP5SCLyellowGP5 is physical pin 7 (I2C0 SCL).
GP4SDAblueGP4 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

main.pyMicroPython
# 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

REPL / Thonny shellOutput
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.

↑↓ to move · Enter to open