devicescript-crear-driver-i2c-raw

How to Create an I2C Driver in DeviceScript

  • 2 min

An I2C driver is the layer that translates a device’s protocol into an interface the program can use. To write one, we need the datasheet and access to the bus.

If a sensor speaks I2C (or SPI), we can talk to it. We only need two things:

  1. Its Datasheet.
  2. The low-level functions i2c.write and i2c.read.

Today we will learn how to create a “Mini-Driver”. It won’t be a complete Jacdac specification (that’s for extra credit), but it will be a class or function that allows us to cleanly use the sensor in our project.

Reading the Datasheet

Before writing code, we need to know what to tell the sensor. For this example, we will implement a driver for the BH1750, a very common light sensor.

If we open its datasheet, we will find a command table like this:

InstructionOpcode (Hex)Description
Power Down0x00Turns off the sensor
Power On0x01Turns on the sensor
Continuous H-Res0x10Starts measuring in high resolution

And it tells us that after sending the measurement command, we must wait 120ms and then read 2 bytes (high byte and low byte) that form the lux value.

We have our plan!

  1. Send 0x10 (Start Measurement).
  2. Wait.
  3. Read 2 bytes.
  4. Combine them mathematically.

Using I2CSensorDriver

The @devicescript/i2c package provides direct bus access and register operations. For a complete driver, it is more convenient to inherit from I2CSensorDriver, which keeps the address and provides writeBuf and readBuf.

Let’s implement the reading in a class. The literal hex`10` creates the buffer with the measurement command without converting it to text.

import { sleep } from "@devicescript/core"
import { I2CSensorDriver } from "@devicescript/drivers"

export class BH1750Driver extends I2CSensorDriver<{ lux: number }> {
    constructor(address = 0x23) {
        super(address, { readCacheTime: 200 })
    }

    override async initDriver() {
        await this.writeBuf(hex`01`) // Power On
    }

    override async readData() {
        await this.writeBuf(hex`10`) // Continuous H-Resolution Mode
        await sleep(180)

        const data = await this.readBuf(2)
        const raw = (data[0] << 8) | data[1]

        return { lux: raw / 1.2 }
    }
}
Copied!

Now we initialize the driver and read the value. read() executes readData() and returns the object with the measurement.

const mySensor = new BH1750Driver(0x23)
await mySensor.init()

setInterval(async () => {
    const { lux } = await mySensor.read()
    console.log(`Light: ${lux.toFixed(1)} lx`)
}, 1000)
Copied!

This example focuses on communication. A reusable driver should validate responses, document the measurement mode, and expose a standard service, for example LightLevel.