Using an AHT20 with Arduino does not require a dedicated sensor library. In this tutorial, the AHT20 is controlled directly using Arduino’s Wire.h I2C interface.
Rather than hiding the sensor communication behind functions supplied by a library, the code sends the AHT20 commands itself, reads the returned bytes and converts the raw 20-bit values into temperature and relative humidity.
Lets go into this
About the AHT20 Temperature and Humidity Sensor
The AHT20 is a digital temperature and relative humidity sensor designed for environmental monitoring applications. Unlike an analogue temperature sensor, the AHT20 performs the sensing and analogue-to-digital conversion internally and makes the finished measurement data available through an I2C interface. This makes it particularly convenient for microcontroller projects because only two signal connections are required: SDA for data and SCL for the clock. The sensor uses the fixed 7-bit I2C address 0x38, so there is normally no need to configure an address before it can be used.
The AHT20 can measure relative humidity across the 0–100% RH range and temperature across approximately -40°C to +85°C. Typical manufacturer specifications quote a humidity accuracy of around ±2% RH and temperature accuracy of around ±0.3°C under normal operating conditions. The sensor operates from a supply of approximately 2.0 V to 5.5 V, although the electrical requirements of the particular breakout board should always be checked. Many AHT20 modules include the required I2C pull-up resistors and other supporting components, which is why they can often be connected directly to an Arduino without additional components.
Internally, the AHT20 does not return temperature and humidity as ready-made decimal numbers. Each measurement is represented by a 20-bit raw value. The microcontroller therefore has to request a measurement, retrieve the bytes returned by the sensor, reconstruct the two 20-bit values and finally convert them into degrees Celsius and percentage relative humidity. Most AHT20 libraries hide all of this, but communicating with the sensor directly provides a useful example of what the library is actually doing.
How the Code Works
The sketch still uses Arduino’s Wire.h, but it does not use an AHT20 library. Wire handles the low-level operation of the microcontroller’s I2C hardware, while the sketch itself deals directly with the AHT20 command protocol. This is a useful middle ground when learning I2C: there is no need to write a complete hardware-specific I2C driver, but the commands, status bits, returned bytes and conversion calculations remain visible.
Communication begins with Wire.begin(), which starts the Arduino’s I2C interface. The AHT20 is then addressed using 0x38. Before measurements are taken, the program reads the sensor’s status byte. One of the bits in this byte indicates whether the device has been calibrated and initialized. If the calibration bit is not set, the sketch sends the initialization sequence 0xBE, 0x08, 0x00 and allows the sensor time to complete the operation.
A measurement is started by transmitting three bytes:
0xAC 0x33 0x00
0xAC tells the AHT20 to perform a measurement, while the following two bytes are parameters required by the measurement command. The result is not available immediately. The sensor needs time to acquire and process the temperature and humidity readings, so the program waits before checking the status byte again.
Bit 7 of the status byte is the busy flag. If this bit is set, the AHT20 is still performing the measurement. The code continues checking it for a limited period rather than assuming that the result must be ready after a fixed delay. Once the busy bit clears, the measurement data can be requested.
The AHT20 supplies seven bytes for a complete measurement response. The first byte contains status information. The next five bytes contain the humidity and temperature measurements, and the final byte contains a CRC value that can be used to check the integrity of the received data.
The slightly unusual part is the way the two measurements are packed into those bytes. Humidity occupies 20 bits and temperature occupies another 20 bits. They do not start and finish neatly on byte boundaries. Part of one byte is used for the bottom four bits of the humidity value while the remaining four bits contain the beginning of the temperature value.
The humidity value is reconstructed with:
uint32_t rawHumidity =
((uint32_t)data[1] << 12) |
((uint32_t)data[2] << 4) |
((uint32_t)data[3] >> 4);
The shifts move each group of bits into its correct position inside a 32-bit integer. The bitwise OR operations then combine them into one 20-bit measurement.
Temperature is reconstructed in much the same way:
uint32_t rawTemperature =
(((uint32_t)data[3] & 0x0F) << 16) |
((uint32_t)data[4] << 8) |
data[5];
The expression data[3] & 0x0F is important because only the lower four bits of that byte belong to the temperature measurement. The upper four bits belong to humidity and must therefore be discarded.
Once the raw humidity value has been assembled, it is converted using:
Relative Humidity = raw value × 100 / 1048576
The number 1,048,576 is 2^20, which corresponds to the full numerical range of the 20-bit measurement.
Temperature uses a similar calculation:
Temperature = (raw value × 200 / 1048576) - 50
The result is temperature in degrees Celsius. These calculations explain why simply printing the bytes returned by the sensor would not produce useful temperature or humidity readings.
Writing the interface this way also makes troubleshooting easier. If the sensor does not respond at address 0x38, the problem is occurring before any temperature calculation takes place. If it responds but remains busy, the measurement sequence can be investigated. If valid bytes are returned but the displayed values are wrong, attention can be directed towards the bit manipulation and conversion calculations.
For a simple sensor, the AHT20 therefore provides a good introduction to direct I2C communication. The finished program is longer than calling a library function such as readTemperature(), but it exposes the complete path from sending a command to the sensor through to converting the returned binary data into a measurement that can be displayed or used elsewhere in a program.
Understanding the AHT20 Data Bytes
When a measurement has finished, the AHT20 returns seven bytes. Temperature and humidity are not returned as ordinary integer or floating-point values. Instead, the sensor provides two 20-bit raw measurements packed across five bytes.
The response has this structure:
Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 +--------+ +--------+ +--------+ +--------+ +--------+ +--------+ +--------+ | STATUS | | H19-H12| | H11-H4 | |H3-H0|T19-T16| T15-T8 | | T7-T0 | | CRC | +--------+ +--------+ +--------+ +--------+ +--------+ +--------+ +--------+
Here, H represents a humidity bit and T represents a temperature bit.
Looking at the measurement portion in more detail:
Humidity ======== Byte 1 Byte 2 Upper half of Byte 3 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 | | | | | | | | | | | | | | | | | | | | H H H H H H H H H H H H H H H H H H H H 19 12 11 4 3 0 Temperature =========== Lower half Byte 3 Byte 4 Byte 5 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | | | | | | | T T T T T T T T T T T T T T T T T T T T 19 16 15 8 7 0
The important byte is data[3]. It is shared between the two measurements:
data[3]
bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|H3 |H2 |H1 |H0 |T19|T18|T17|T16|
+---+---+---+---+---+---+---+---+
HUMIDITY TEMPERATURE
This is why the values cannot simply be assembled from complete bytes.
For humidity, the program takes all eight bits from data[1], all eight bits from data[2], and only the upper four bits from data[3]:
uint32_t rawHumidity =
((uint32_t)data[1] << 12) |
((uint32_t)data[2] << 4) |
((uint32_t)data[3] >> 4);
The first shift:
(uint32_t)data[1] << 12
moves the first eight humidity bits into positions 19 through 12.
The second:
(uint32_t)data[2] << 4
moves the next eight bits into positions 11 through 4.
Finally:
data[3] >> 4
shifts the upper nibble of byte 3 down into positions 3 through 0.
The three pieces can then be combined with the bitwise OR operator:
HHHH HHHH 0000 0000 0000 0000 0000 HHHH HHHH 0000 0000 0000 0000 0000 HHHH ------------------------- HHHH HHHH HHHH HHHH HHHH
The result is one complete 20-bit humidity value.
Temperature works in the opposite direction because its first four bits occupy the lower half of data[3]:
uint32_t rawTemperature =
(((uint32_t)data[3] & 0x0F) << 16) |
((uint32_t)data[4] << 8) |
data[5];
The expression:
data[3] & 0x0F
uses a bit mask to remove the humidity bits.
In binary, 0x0F is:
00001111
Applying the mask gives:
data[3] H H H H T T T T
mask 0 0 0 0 1 1 1 1
----------------
result 0 0 0 0 T T T T
Only the four temperature bits remain. They are shifted left by 16 positions, data[4] is shifted left by eight positions, and data[5] already occupies the correct position.
The finished value therefore becomes:
TTTT TTTT TTTT TTTT TTTT 19 0
This gives the 20-bit raw temperature measurement.
Converting the Raw Values
A 20-bit number can represent 1,048,576 different values:
2^20 = 1,048,576
The AHT20 uses this range to represent its measurement span. Humidity is converted into percentage relative humidity with:
humidity = ((float)rawHumidity * 100.0) / 1048576.0;
For example, a raw value close to half of the full 20-bit range:
524288
would give:
524288 × 100
---------------- = 50%
1048576
Temperature is scaled across a 200°C range and then offset by -50°C:
temperature =
((float)rawTemperature * 200.0 / 1048576.0) - 50.0;
A raw value of 524288 is again exactly halfway through the available digital range:
524288 × 200
---------------- - 50
1048576
which produces:
100 - 50 = 50°C
This byte packing is one of the main reasons an AHT20 library can appear to make the sensor much simpler than it actually is. A library may reduce the whole operation to a function call, but underneath it still has to send the measurement command, wait for the sensor, retrieve these bytes, separate the two 20-bit values and apply the appropriate conversion formulas.
Working through the bytes directly is also a useful introduction to bit shifting and masking. These techniques appear frequently when working with sensors, ADCs, configuration registers and other embedded hardware, particularly when a value is wider than eight bits or several pieces of information share the same byte.
Code
#include <Wire.h>
#define AHT20_ADDRESS 0x38
void setup()
{
Serial.begin(115200);
Wire.begin();
delay(100); // AHT20 needs delay after power-up
// Check sensor status
uint8_t status = readStatus();
Serial.print("Initial status: 0x");
Serial.println(status, HEX);
// Bit 3 indicates calibration state
if ((status & 0x08) == 0)
{
Serial.println("AHT20 not calibrated - initializing...");
initializeAHT20();
delay(10);
}
Serial.println("AHT20 ready.");
}
void loop()
{
float temperature;
float humidity;
if (readAHT20(temperature, humidity))
{
Serial.print("Temperature: ");
Serial.print(temperature, 2);
Serial.println(" C");
Serial.print("Humidity: ");
Serial.print(humidity, 2);
Serial.println(" %");
Serial.println();
}
else
{
Serial.println("Failed to read AHT20.");
}
delay(2000);
}
// ------------------------------------------------------
// Read temperature and humidity
// ------------------------------------------------------
bool readAHT20(float &temperature, float &humidity)
{
// Send measurement command
Wire.beginTransmission(AHT20_ADDRESS);
Wire.write(0xAC);
Wire.write(0x33);
Wire.write(0x00);
if (Wire.endTransmission() != 0)
{
return false;
}
// Typical measurement takes about 80 ms
delay(80);
// Check busy flag
uint8_t status = readStatus();
int timeout = 100;
while ((status & 0x80) && timeout > 0)
{
delay(2);
status = readStatus();
timeout -= 2;
}
if (status & 0x80)
{
return false;
}
// AHT20 returns 7 bytes:
//
// Byte 0 : status
// Byte 1 : humidity bits 19..12
// Byte 2 : humidity bits 11..4
// Byte 3 : humidity bits 3..0 + temperature bits 19..16
// Byte 4 : temperature bits 15..8
// Byte 5 : temperature bits 7..0
// Byte 6 : CRC
Wire.requestFrom(AHT20_ADDRESS, 7);
if (Wire.available() != 7)
{
return false;
}
uint8_t data[7];
for (int i = 0; i < 7; i++)
{
data[i] = Wire.read();
}
// Check busy bit again
if (data[0] & 0x80)
{
return false;
}
// Extract 20-bit humidity value
uint32_t rawHumidity =
((uint32_t)data[1] << 12) |
((uint32_t)data[2] << 4) |
((uint32_t)data[3] >> 4);
// Extract 20-bit temperature value
uint32_t rawTemperature =
(((uint32_t)data[3] & 0x0F) << 16) |
((uint32_t)data[4] << 8) |
data[5];
// Convert raw values
humidity =
((float)rawHumidity * 100.0) / 1048576.0;
temperature =
((float)rawTemperature * 200.0 / 1048576.0) - 50.0;
return true;
}
// ------------------------------------------------------
// Read status register
// ------------------------------------------------------
uint8_t readStatus()
{
Wire.requestFrom(AHT20_ADDRESS, 1);
if (Wire.available())
{
return Wire.read();
}
return 0xFF;
}
// ------------------------------------------------------
// Initialize / calibrate AHT20
// ------------------------------------------------------
void initializeAHT20()
{
Wire.beginTransmission(AHT20_ADDRESS);
Wire.write(0xBE);
Wire.write(0x08);
Wire.write(0x00);
Wire.endTransmission();
}
For a typical Arduino Uno/Nano, connect the AHT20 as follows:
| AHT20 | Arduino Uno/Nano |
|---|---|
| VCC | 3.3 V or 5 V depending on breakout |
| GND | GND |
| SDA | A4 |
| SCL | A5 |
The important calculation is that the sensor doesn’t send temperature as an ordinary integer. It returns a 20-bit ADC-style value. The conversion is:
Humidity (%) = rawHumidity × 100 / 2^20 Temperature (°C) = rawTemperature × 200 / 2^20 - 50
where 2^20 = 1,048,576.
Serial monitor
This is what I saw
Temperature: 22.14 C Humidity: 54.20 %
Temperature: 22.07 C Humidity: 54.37 %


