Skip to content

Overview

The BH1750 is an ambient light sensor read over the I2C bus (a two-wire serial communication — one clock SCL, one data SDA — one of the most common interfaces for sensors); ambient light is "how bright the environment is", measured in lux: noon sunlight is about 100,000 lux, indoor lighting about 100~500 lux. This tutorial uses the Ai-WB2 to read the BH1750's ambient light over I2C, printing it on the serial in real time, and walks through the full flow of wiring → coding → building → flashing (writing the compiled program into the board's chip) → running and verification.

In plain words: the BH1750 is a sensor with a built-in "light-sensing little eye". The main controller (the board) finds its "door number" (I2C address) through two wires (I2C is like two wires in a hallway: one shouts numbers, one passes messages), shouts "start measuring!" (sends a measurement command), and it reports the current brightness. Shine a flashlight on it and the value jumps up; cover it with a black cloth and it drops.

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/iot-solution/demo_bh1750; the code can be found directly in the local SDK.

🎯Page GoalRead the BH1750 ambient light over I2C, print the light value on the serial every second, and master I2C master send-command and receive-data.
🧰Prerequisites① An Ai-WB2 development board + a BH1750 ambient light sensor module ② Development environment set up per [SDK Installation](../sdk/sdk_intro), and [GPIO Output (LED)](../basic/gpio_led) done.
🔗RelatedI2C fundamentals: [I2C Protocol](../basic/i2c); a sensor in the same series: [DHT11 Temperature and Humidity Sensor](./dht11).

Hardware Wiring

Wire per the official example (see the SDK’s applications/iot-solution/demo_bh1750/README.md), using dupont wires (jumper wires with pins at both ends):

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

💡 The BH1750 is an I2C device — VCC to 3.3V (some modules tolerate 5V, but this tutorial uses 3.3V throughout to avoid level mismatch). 💡 I2C’s SCL/SDA are open-drain signals (a pin can only actively pull low, not high), so pull-up resistors (small resistors that hold the line high by default) are required; most BH1750 modules have onboard pull-ups; for a bare chip, add a 4.7kΩ pull-up on each of SCL and SDA to 3V3.

Enter the Example Project

This tutorial directly uses the demo_bh1750 example project shipped with the official SDK; open a terminal and enter it:

cd ~/Ai-Thinker-WB2/applications/iot-solution/demo_bh1750

Note: cd is the “change directory” command — entering the BH1750 example project directory; all subsequent make build and make flash flash commands must run in this directory first.

Project structure:

File Purpose
demo_bh1750/main.c Main program source, the main file this tutorial looks at
Makefile Build entry, usually no changes needed
proj_config.mk Project config (flash size, feature switches, etc.), usually no changes needed
Write the Code

Open demo_bh1750/main.c — the complete 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/iot-solution/demo_bh1750/demo_bh1750/main.c).

Code highlights:

Code Purpose
.scl = 12.sda = 3.freq = 100000 I2C pins IO12/IO3, clock 100kHz (standard mode); wrong pins = no communication
BH1750_DEFAULT_ADDR 0x23 The BH1750’s I2C address (door number): ADDR pin floating/low is 0x23, tied high is 0x5c; wrong address = sensor not found
cmd = BH1750_ONETIME_H_MODE (0x20) Sends the “one-time high-resolution measurement” command, telling the sensor to start measuring; without the command the sensor does nothing
hosal_i2c_master_send(&i2c0, addr, &cmd, 1, HOSAL_WAIT_FOREVER) The master sends the 1-byte measurement command to the sensor; waits forever on send failure
hosal_i2c_master_recv(&i2c0, addr, buffer, 2, 100) Receives 2 bytes of raw light value (high byte first); no receive = no light value
result /= 1.2f BH1750 resolution is 1.2 lux/bit; true light = raw value ÷ 1.2 (the official code prints the raw value read)
Build the Project

Build in the project directory:

make -j8

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

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

⚠️ If it reports riscv64-unknown-elf-gcc: command not found, the toolchain permissions aren’t configured — run cd toolchain/riscv/Linux && . chmod755.sh first, then rebuild.

Flash the Firmware

Keep the board connected via USB, confirm the serial device (usually /dev/ttyUSB0 on Linux), 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 one — 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 (some boards enter automatically); wait for the progress bar to complete — that means the flash succeeded. For flashing on Windows, see Windows Quick Start.

Run and Verify

After flashing, the board automatically restarts and runs; open the serial assistant (baud rate 921600 — the baud rate is the serial transfer speed, both ends must be set the same) and the light value prints every 1 second:

lux level: 123.00
lux level: 124.00
...

Shine a flashlight on the sensor — the value should jump up; cover it with a black cloth — the value should drop.

Seeing the light value print every second, changing with brightness, means success; if it keeps printing i2c timeout or the value is fixed, it hasn’t succeeded yet — check the FAQ at the end.

💡 Watch the IO12/IO3 waveforms with a logic analyzer to see the I2C START, address, data and STOP timing (see the official example’s 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 (this tutorial initializes master mode to read the BH1750).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer, required. Key fields: config.mode (HOSAL_I2C_MODE_MASTER master / HOSAL_I2C_MODE_SLAVE slave), config.scl/config.sda (SCL/SDA pin numbers, this tutorial IO12/IO3), config.freq (100000 standard 100kHz / 400000 fast 400kHz), config.address_width (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)

As the master, sends a frame of data to the slave at the given address (including start/address/stop bits; this tutorial sends the BH1750 measurement command).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer
  • dev_addr: slave device address (the byte after shifting the 7-bit address left by 1), e.g. 0x23
  • data: send data buffer pointer, required
  • size: bytes to send, values: 1~256
  • timeout: wait timeout (ms), values: e.g. 100; HOSAL_WAIT_FOREVER (0xFFFFFFFF) means wait forever

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

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

As the master, reads a frame of data from the given slave (this tutorial receives the BH1750's 2-byte raw light value).

Parameters:

  • i2c: hosal_i2c_dev_t struct pointer
  • dev_addr: slave device address (same rule as master_send)
  • data: receive buffer pointer, required
  • size: expected bytes to receive (this tutorial 2)
  • timeout: wait timeout (ms); returns failure if nothing arrives in time

Return: 0 on success; negative error code on failure (this tutorial judges timeout from this and prints i2c timeout)

blog_info(fmt, ...)

Prints an INFO-level log (UART0, subject to level filtering); this tutorial prints the light value with it.

Parameters:

  • fmt: format string, same usage as printf, required
  • ...: variadic args matching the fmt placeholders; can be omitted

Return: none

blog_error(fmt, ...)

Prints an ERROR-level error log; this tutorial prints i2c timeout on I2C timeout (sensor unreadable).

Parameters:

  • fmt: format string, same usage as printf, required
  • ...: variadic args matching the fmt placeholders; can be omitted

Return: none

vTaskDelay(ms)

Suspends the current task for the given milliseconds, yielding the CPU to other tasks.

Parameters:

  • ms: delay in milliseconds, values: any non-negative integer (internally converted to system ticks via pdMS_TO_TICKS)

Return: none


Full Code

Below is the complete demo_bh1750/main.c source, identical to the official example (applications/iot-solution/demo_bh1750/demo_bh1750/main.c):

📜 Click to expand the full demo_bh1750/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 BH1750_DEFAULT_ADDR BH1750_ADDR_L
#define BH1750_ADDR_H 0x5c
#define BH1750_ADDR_L 0x23
#define BH1750_POWER_DOWN 0x00
#define BH1750_POWER_ON 0x01
#define BH1750_RESET 0x07
#define BH1750_CONTINUOUS_H_MODE  0x10
#define BH1750_CONTINUOUS_H_MODE2  0x11
#define BH1750_CONTINUOUS_L_MODE  0x13
#define BH1750_ONETIME_H_MODE  0x20
#define BH1750_ONETIME_H_MODE2  0x21
#define BH1750_ONETIME_L_MODE  0x23

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 (;;) {
        
        uint8_t buffer[2];
        uint8_t cmd = BH1750_ONETIME_H_MODE;
        hosal_i2c_master_send(&i2c0, BH1750_DEFAULT_ADDR, &cmd, 1, HOSAL_WAIT_FOREVER);
        int ret = hosal_i2c_master_recv(&i2c0, BH1750_DEFAULT_ADDR, buffer, 2, 100);
        if (ret) {
            cmd = BH1750_POWER_ON;
            hosal_i2c_master_send(&i2c0, BH1750_DEFAULT_ADDR, &cmd, 1, 100);
            blog_error("i2c timeout\r\n");
        }
        else {
            uint16_t result = buffer[0];
            result <<= 8;
            result |= buffer[1];

            float luxlevel = result;
            result /= 1.2f;

            blog_info("lux level: %.02f\r\n", luxlevel);
        }

        vTaskDelay(portTICK_RATE_MS * 1000);
    }

    return 0;
}

FAQ & Troubleshooting

⚠️ Keeps printing i2c timeout (no data)
Cause: wrong wiring, no pull-up resistors on SCL/SDA, insufficient module power, or a damaged sensor
Fix: check IO12/IO3/3V3/GND against the wiring table one by one; confirm 4.7kΩ pull-ups on SCL/SDA; try another module

⚠️ Light value always 0 or garbled
Cause: insufficient VCC supply, no common ground between the board and module (GND not connected), or poor dupont wire contact
Fix: confirm VCC to 3V3 and GND to GND (common ground = the two devices' grounds must be joined, otherwise voltage has no reference point); re-seat the dupont wires

⚠️ Light value fixed (no reaction to shading/bright light)
Cause: the sensor didn't receive the measurement command (wrong command byte), or the module's ADDR pin state doesn't match the code (tied high makes the address 0x5c)
Fix: confirm the module's ADDR is floating or low (address 0x23); if ADDR is tied to 3V3, change BH1750_DEFAULT_ADDR to BH1750_ADDR_H (0x5c) and rebuild/reflash

⚠️ Serial device not found or no permission
Cause: on Linux /dev/ttyUSB0 doesn't exist or permission denied; on Windows the USB-to-serial driver isn't installed
Fix: on Linux check with ls /dev/ttyUSB*; if permission denied run sudo usermod -aG dialout $USER and log back in; on Windows install the driver in Device Manager and confirm the COM port

⚠️ Flashing keeps waiting, 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 during flashing to enter download mode as prompted; try a Type-C data-capable cable

⚠️ make reports Makefile not found
Cause: the build command ran in the wrong directory (must be inside the example project directory)
Fix: run cd ~/Ai-Thinker-WB2/applications/iot-solution/demo_bh1750 first, then make -j8

Self-Check

The serial prints a light value every second; a flashlight makes it jump up, shading makes it drop — the BH1750 ambient light measurement is verified.

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