Skip to content

Overview

I2C (Inter-Integrated Circuit) is a serial bus where multiple devices hang off just two wires (SCL clock + SDA data); every slave has a unique 7-bit address, making it one of the most common sensor interfaces. This tutorial reads the SHT30 temperature/humidity sensor via I2C, parsing temperature and humidity with CRC verification.

In plain words: I2C is like two wires laid in a "corridor": one calls out numbers (SCL clock), the other passes messages (SDA data). Every device in the corridor has its own "door number" (7-bit address); whichever number is called comes out to answer. This tutorial has the main controller asking the SHT30 sensor for temperature and humidity through these two wires.

This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version release_bl_iot_sdk_1.6.40) example applications/peripherals/demo_i2c; the code can be found directly in your local SDK.

🎯Page GoalRead the SHT30 temperature/humidity sensor via the I2C bus and print temperature and humidity over serial; learn I2C init, master send/receive and data parsing.
🧰Prerequisites① Ai-WB2 development board, SHT30 temperature/humidity sensor module ② Environment set up per [SDK Installation](../sdk/sdk_intro) and [GPIO Output (Blink LED)](./gpio_led) completed.
🔗RelatedOther I2C sensor applications: [Best Practices - BH1750](../best/bh1750); the SPI bus: [SPI Protocol](./spi).

Hardware Wiring

Wire per the official example (see SDK applications/peripherals/demo_i2c/README.md):

Ai-WB2 Pin SHT30 Pin
IO12 SCL
IO3 SDA
3V3 VCC
GND GND

💡 I2C’s SCL/SDA are open-drain signals (the pin can only actively pull low, not pull high), so they need pull-up resistors (small resistors that hold the line high by default). Most SHT30 modules have pull-ups on board; for a bare chip, connect a 4.7kΩ pull-up from each of SCL/SDA to 3V3.

Enter the Example Project

Open a terminal and enter the official demo_i2c example project directory:

cd ~/Ai-Thinker-WB2/applications/peripherals/demo_i2c

Note: cd is the “change directory” command; this enters the demo_i2c example project. All subsequent make build and make flash commands must run inside this directory first.

Write the Code

Open demo_i2c/main.c. The full code for this step has been moved to the end of this page:

📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, identical to the official example (applications/peripherals/demo_i2c/demo_i2c/main.c).

Code highlights:

Code Purpose
.scl = 12, .sda = 3, .freq = 100000 I2C pins IO12/IO3, clock 100kHz (standard mode); wrong pins and communication fails
.mode = HOSAL_I2C_MODE_MASTER Master mode (the board is the “speaker” asking questions, the sensor the “responder”); configured as slave, nobody initiates communication
SHT31_DEFAULT_ADDR 0x0044 SHT30’s 7-bit address (door number: 0x44 when ADDR is floating/low, 0x45 when high); a wrong address means the sensor is never found
hosal_i2c_master_send(&i2c0, addr, command, 2, 100) Sends the 2-byte measurement command 0x2400 (telling the sensor to start measuring); without it the sensor does nothing
hosal_i2c_master_recv(&i2c0, addr, &data, 6, 100) Receives 6 bytes: temp 2B+CRC, humidity 2B+CRC; no data received means no temperature/humidity reading
crc8(...) CRC8 check with polynomial 0x31 to catch transfer errors; without it bad data is treated as real values

Temperature/humidity conversion formulas (SHT30 datasheet):

Data Conversion Formula
Temperature T = raw temp value × 17500 ÷ 0xFFFF - 4500, unit 0.01°C
Humidity RH = raw humidity value × 10000 ÷ 0xFFFF, unit 0.01%RH
Build the Project

Build in the project directory:

make -j8

Note: make is the “build” command, turning code into firmware (a program file) the board can run; -j8 builds with 8 parallel cores, faster.

On success a firmware build_out/demo_i2c.bin is generated.

Flash the Firmware

Keep the board connected via USB, confirm the serial device, and flash:

make flash p=/dev/ttyUSB0 b=921600

Note: make flash is the “flash” command, writing the compiled firmware into the board’s chip. After p= comes the serial device (change it to your computer’s actual port — check with ls /dev/ttyUSB*); b= is the flash baud rate (transfer speed).

⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode; wait for the progress bar to complete — that means the flash succeeded.

Run and Verify

After flashing, the board automatically restarts. Open a serial assistant (baud rate 921600); temperature and humidity print once per second:

temperature: 26.54 C	humidity: 45 %
temperature: 26.53 C	humidity: 45 %
...

Breathe on the sensor and the humidity should visibly rise; squeeze the sensor in your hand and the temperature should slowly rise. N/A prints when the CRC check fails.

Seeing a temperature/humidity print every second with values updating with the environment means success; if N/A keeps printing or the value never changes, check the “FAQ & Troubleshooting” section at the end.

💡 Observe the IO12/IO3 waveforms with a logic analyzer to see I2C’s START, address, data and STOP timing (see the official example img/logic_analyzer.jpg).


API Summary for This Tutorial

hosal_i2c_init(i2c)

Configures the I2C controller per the pins, frequency and master/slave mode in the dev struct (initializing master mode to read the SHT30 in this tutorial).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer, required. Key fields: config.mode (values: HOSAL_I2C_MODE_MASTER master / HOSAL_I2C_MODE_SLAVE slave), config.scl/config.sda (SCL/SDA pin numbers, IO12/IO3 here), config.freq (values: 100000 standard 100kHz / 400000 fast 400kHz), config.address_width (values: HOSAL_I2C_ADDRESS_WIDTH_7BIT / HOSAL_I2C_ADDRESS_WIDTH_10BIT)

Return: 0 on success; negative error code on failure

hosal_i2c_master_send(i2c, dev_addr, data, size, timeout)

Sends one frame as master to the slave at the given address (start/address/stop included; sends the SHT30 measurement command in this tutorial).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer
  • dev_addr: slave device address (the byte after the 7-bit address shifted left 1), e.g. 0x5c << 1
  • data: pointer to the send buffer, required
  • size: byte count to send, values: 1~256
  • timeout: wait timeout (unit ms), values: e.g. 100

Return: 0 on success; negative error code on failure (no ACK/timeout)

hosal_i2c_master_recv(i2c, dev_addr, data, size, timeout)

Reads one frame as master from the given slave (receiving the SHT30's 6-byte temperature/humidity data in this tutorial).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer
  • dev_addr: slave device address (same rule as master_send)
  • data: pointer to the receive buffer, required
  • size: expected byte count
  • timeout: wait timeout (unit ms)

Return: 0 on success; negative error code on failure

hosal_i2c_mem_write(i2c, dev_addr, mem_addr, data, size, timeout)

Writes to a slave's internal register: sends the register address first, then the data (commonly used for sensor configuration).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer
  • dev_addr: slave device address
  • mem_addr: slave register address (8-bit register address, values: 0x00~0xFF, see the chip datasheet)
  • data: pointer to the buffer to write
  • size: byte count to write
  • timeout: wait timeout (unit ms)

Return: 0 on success; negative error code on failure

hosal_i2c_mem_read(i2c, dev_addr, mem_addr, data, size, timeout)

Sends the register address to the slave first, then reads that register's content (commonly used for reading sensor data).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer
  • dev_addr: slave device address
  • mem_addr: slave register address (values: 0x00~0xFF)
  • data: pointer to the receive buffer, required
  • size: expected byte count
  • timeout: wait timeout (unit ms)

Return: 0 on success; negative error code on failure

hosal_i2c_finalize(i2c)

Stops the I2C controller and frees the occupied pins (call before no longer using it).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer

Return: 0 on success; negative error code on failure

blog_info(fmt, ...)

Outputs an INFO-level log (UART0, filtered by level).

Parameters:

  • fmt: format string, same usage as printf, required
  • ...: variadic args matching fmt placeholders, optional

Return: none


Full Code

Below is the complete demo_i2c/main.c source, identical to the official example (applications/peripherals/demo_i2c/demo_i2c/main.c):

📜 Click to expand the full demo_i2c/main.c code
c
#include <stdio.h>

#include <FreeRTOS.h>
#include <task.h>

#include <hosal_i2c.h>
#include <bl_gpio.h>
#include <blog.h>

#define SHT31_DEFAULT_ADDR 0x0044
#define SHT31_MEAS_HIGHREP 0x2400

#pragma pack(1)
struct sht3x_data
{
    uint8_t st_high;
    uint8_t st_low;
    uint8_t st_crc8;
    uint8_t srh_high;
    uint8_t srh_low;
    uint8_t srh_crc8;
};
#pragma pack()

static uint8_t crc8(uint8_t *data, int len)
{
    const uint8_t POLYNOMIAL = 0x31;
    uint8_t crc = 0xFF;
    for (int j = len; j; --j)
    {
        crc ^= *data++;
        for (int i = 8; i; --i)
        {
            crc = (crc & 0x80)
                      ? (crc << 1) ^ POLYNOMIAL
                      : (crc << 1);
        }
    }
    return crc;
}

int main(void)
{
    static hosal_i2c_dev_t i2c0 = {
        .config = {
            .address_width = HOSAL_I2C_ADDRESS_WIDTH_7BIT,
            .freq = 100000,
            .mode = HOSAL_I2C_MODE_MASTER,
            .scl = 12,
            .sda = 3,
        },
        .port = 0,
    };

    hosal_i2c_init(&i2c0);

    for (;;) {
        
        struct sht3x_data data;

        uint8_t command[2] = { SHT31_MEAS_HIGHREP >> 8, SHT31_MEAS_HIGHREP & 0xff };
        hosal_i2c_master_send(&i2c0, SHT31_DEFAULT_ADDR, command, sizeof command, 100);
        hosal_i2c_master_recv(&i2c0, SHT31_DEFAULT_ADDR, (uint8_t*)&data, sizeof data, 100);

        char temperature_str[8];
        char humidity_str[8];

        if (crc8(&data.st_high, 2) == data.st_crc8) {
            uint16_t st = data.st_high;
            st <<= 8;
            st |= data.st_low;

            int temp = st;
            temp *= 17500;
            temp /= 0xffff;
            temp = -4500 + temp;

            int temperature_integer = temp / 100;
            
            if (temp < 0) {
                temp = -temp;
            }

            unsigned temperature_decimal = temp % 100;

            sprintf(temperature_str, "%d.%02u C", temperature_integer, temperature_decimal);
        }
        else {
            sprintf(temperature_str, "%s", "N/A C");
        }
        
        if (crc8(&data.srh_high, 2) == data.srh_crc8) {
            uint16_t srh = data.srh_high;
            srh <<= 8;
            srh |= data.srh_low;

            unsigned humidity = srh;
            humidity *= 10000;
            humidity /= 0xFFFF;

            unsigned humidity_integer = humidity / 100;

            sprintf(humidity_str, "%u %%", humidity_integer);
        }
        else {
            sprintf(humidity_str, "N/A %%");
        }

        blog_info("temperature: %s\thumidity: %s\r\n", temperature_str, humidity_str);

        vTaskDelay(portTICK_RATE_MS * 1000);
    }

    return 0;
}

FAQ & Troubleshooting

⚠️ Prints N/A (CRC check failed)
Cause: poor contact in wiring, no pull-up resistors on SDA/SCL, or insufficient module power
Fix: confirm 4.7kΩ pull-ups on SDA/SCL; shorten the jumper wires; confirm the module's VCC connects to 3V3 with common ground

⚠️ Data is 0 or never changes
Cause: wrong slave address (SHT30's ADDR pin connected to 3V3 changes the address to 0x45)
Fix: confirm the module's ADDR state; change SHT31_DEFAULT_ADDR to 0x0045 or the matching address

⚠️ Read fails after switching sensors
Cause: different I2C sensors have different command formats and register addresses
Fix: check the target sensor's datasheet first; use hosal_i2c_mem_read/mem_write to read/write by register address (demo_i2c uses the SHT3x private protocol without register addresses)

⚠️ IO12/IO3 conflict with other functions
Cause: IO12/IO3 are also used by multiplexed functions such as SPI/ADC
Fix: confirm no other module in the project uses these two pins; or change .scl/.sda to other available pins

⚠️ Flashing keeps waiting, the progress bar doesn't move
Cause: download mode wasn't entered, or the cable only charges and can't transfer data
Fix: press and hold EN to enter download mode when prompted; try a Type-C cable that can transfer data and retry

⚠️ Serial device not found or permission denied
Cause: /dev/ttyUSB0 doesn't exist or permissions are insufficient on Linux; USB-to-serial driver not installed on Windows
Fix: on Linux confirm the device with ls /dev/ttyUSB*, for permissions run sudo usermod -aG dialout $USER then log in again; on Windows install the driver in Device Manager and confirm the COM number

Self-Check

The serial prints temperature/humidity every second, with values updating with the environment — I2C communication is verified.

Released under the MIT License. Build Time 2026-09-11 14:52:23