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) exampleapplications/iot-solution/demo_bh1750; the code can be found directly in the local SDK.
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.
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:
cdis the “change directory” command — entering the BH1750 example project directory; all subsequentmakebuild andmake flashflash 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 |
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 in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (the program file) the board can run;-j8builds 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 — runcd toolchain/riscv/Linux && . chmod755.shfirst, then rebuild.
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 flashis the “flash” command, writing the compiled firmware into the board’s chip. Afterp=comes the serial device (change it to your computer’s actual one — check withls /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.
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_tstruct pointer, required. Key fields:config.mode(HOSAL_I2C_MODE_MASTERmaster /HOSAL_I2C_MODE_SLAVEslave),config.scl/config.sda(SCL/SDA pin numbers, this tutorial IO12/IO3),config.freq(100000standard 100kHz /400000fast 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_tstruct pointerdev_addr: slave device address (the byte after shifting the 7-bit address left by 1), e.g.0x23data: send data buffer pointer, requiredsize: bytes to send, values:1~256timeout: 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_tstruct pointerdev_addr: slave device address (same rule asmaster_send)data: receive buffer pointer, requiredsize: expected bytes to receive (this tutorial2)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 asprintf, required...: variadic args matching thefmtplaceholders; 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 asprintf, required...: variadic args matching thefmtplaceholders; 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 viapdMS_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
#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.

