# Writing a Linux IIO Driver for the MS8607 Sensor

This post covers the development of a Linux kernel driver for the MS8607 pressure, humidity, and temperature sensor on the BeagleBone Black. The driver integrates with the IIO (Industrial I/O) subsystem to provide a standard interface for userspace applications.

**Repository**: https://github.com/billvanleeuwen424/ms8607-driver

## Background

Device drivers bridge hardware and the operating system. Without a driver, the kernel can't communicate with hardware devices. For the MS8607, the driver needs to:

* Communicate over I2C with two sensor addresses (0x76 for pressure/temp, 0x40 for humidity)
    
* Read factory calibration data from PROM and validate with CRC
    
* Convert raw ADC values to pressure (mbar), temperature (°C), and humidity (%RH)
    
* Expose data through IIO's sysfs interface at `/sys/bus/iio/devices/`
    

## Hardware Setup

I utilized this pinout diagram from element14 [**here**](https://community.element14.com/products/devtools/single-board-computers/next-genbeaglebone/b/blog/posts/controlling-the-beaglebone-black-s-gpio-pins-from-the-web-using-drupal)[.](https://community.element14.com/products/devtools/single-board-computers/next-genbeaglebone/b/blog/posts/controlling-the-beaglebone-black-s-gpio-pins-from-the-web-using-drupal)

The MS8607 is mounted on an Adafruit breakout board and connected to the BeagleBone Black I2C2 bus:

```bash
P9_19 (I2C2_SCL) → SCL
P9_20 (I2C2_SDA) → SDA
P9_3  (3.3V)     → VIN
P9_1  (GND)      → GND
```

I had the board connected up like so on this very professional breadboard:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763332962338/f06c757a-fb0e-4e1f-be4f-52d358f09254.jpeg align="center")

### Validating I2C Communication

Standard practice is to run `i2cdetect` first:

```bash
sudo i2cdetect -y 2
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
```

Nothing showed up. The MS8607 doesn't respond to `i2cdetect` probing—it requires specific command sequences. Manual validation with `i2cget` and `i2cset` confirmed both sensors were functional

```bash
# Test humidity sensor at 0x40
i2cget -y 2 0x40 0xE7
# Output: 0x02

# Test pressure/temp sensor at 0x76
i2cset -y 2 0x76 0x5A
i2ctransfer -y 2 w1@0x76 0x00 r3
# Output: 0x85 0x4f 0x37
```

## Device Tree Configuration

I had to learn a lot about device trees for this project. Again, I recommend Bootlin documentation extremely useful! I utilized this slide deck on [Device Tree 101 from Bootlin.](https://bootlin.com/pub/conferences/2021/webinar/petazzoni-device-tree-101/petazzoni-device-tree-101.pdf)

The MS8607 contains two sensors on one chip with different I2C addresses. This presented an architecture choice: write two separate drivers (one per sensor) or a single driver managing both.

### Initial Approach

The first device tree had two nodes, both enabled:

```bash
ms8607-pt@76 {
    compatible = "te,ms8607-pt";
    reg = <0x76>;
    status = "okay";
};

ms8607-h@40 {
    compatible = "te,ms8607-h";
    reg = <0x40>;
    status = "okay";
};
```

### The Problem

With both nodes at `status = "okay"`, the kernel instantiates I2C devices at both addresses during boot. When the driver tried to create a dummy client for 0x40 using `devm_i2c_new_dummy_device()`, it failed with `-EBUSY` (error -16)—the address was already taken.

### Solution

Only the primary sensor at 0x76 has `status = "okay"`. The secondary sensor is documented but disabled:

```bash
/* Primary - driver binds here */
ms8607@76 {
    compatible = "te,ms8607";
    reg = <0x76>;
    status = "okay";
};

/* Secondary - documented but disabled */
ms8607-h@40 {
    compatible = "te,ms8607-humidity";
    reg = <0x40>;
    status = "disabled";
};
```

The driver creates the humidity sensor client internally. This exposes all three channels (pressure, temperature, humidity) through a single IIO device.

## Driver Implementation

### Development Tools

I used Claude Code for parts of this project—parsing datasheet formulas, generating the CI/CD pipeline, and discussing architecture trade-offs. It was useful for accelerating tasks that would normally require reading documentation or trial-and-error. Hardware debugging and sensor validation still required hands-on work with the BeagleBone.

### Calibration and CRC Validation

The pressure/temperature sensor stores factory calibration coefficients (C1-C6) in PROM. These coefficients are used in the datasheet's formulas to convert raw ADC values into calibrated pressure and temperature readings. If the coefficients are corrupted, all sensor readings will be wrong.

The driver reads the PROM once during probe and validates the data with a 4-bit CRC:

```c
static int ms8607_load_calibration(struct ms8607_data *data)
{
    int ret;
    u16 prom[8];

    ret = ms8607_read_prom_words(data->client, prom);
    if (ret)
        return ret;

    if (!ms8607_validate_prom_crc(prom)) {
        dev_err(&data->client->dev, "PROM CRC validation failed\n");
        return -EIO;
    }

    memcpy(data->calibration, prom, sizeof(data->calibration));
    data->calibration_valid = true;
    return 0;
}
```

### Sensor Readings

Each sensor requires triggering an ADC conversion, waiting for completion, then reading the result.

**Humidity** (16-bit):

```c
// Trigger no-hold measurement
i2c_smbus_write_byte(client_h, 0xF5);
msleep(9);  // ADC conversion time

// Read result
adc = i2c_smbus_read_word_data(client_h, 0xE5);

// Convert to 0.01%RH units
humidity = -600 + ((12500 * adc) >> 16);
```

**Pressure** (24-bit, requires temperature compensation):

```c
// Trigger temperature conversion
i2c_smbus_write_byte(client, 0x58);
msleep(9);

// Read D2 (raw temperature)
i2c_smbus_write_byte(client, 0x00);
i2c_smbus_read_i2c_block_data(client, 0x00, 3, buf);
D2 = (buf[0] << 16) | (buf[1] << 8) | buf[2];

// Trigger pressure conversion
i2c_smbus_write_byte(client, 0x48);
msleep(9);

// Read D1 (raw pressure)
i2c_smbus_write_byte(client, 0x00);
i2c_smbus_read_i2c_block_data(client, 0x00, 3, buf);
D1 = (buf[0] << 16) | (buf[1] << 8) | buf[2];

// Apply calibration (datasheet formulas)
dT = D2 - (C5 << 8);
TEMP = 2000 + ((dT * C6) >> 23);
OFF = (C2 << 17) + ((C4 * dT) >> 6);
SENS = (C1 << 16) + ((C3 * dT) >> 7);
P = (((D1 * SENS) >> 21) - OFF) >> 15;
```

### IIO Integration

The driver registers three channels:

```c
static const struct iio_chan_spec ms8607_channels[] = {
    {
        .type = IIO_PRESSURE,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
    },
    {
        .type = IIO_TEMP,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
    },
    {
        .type = IIO_HUMIDITYRELATIVE,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
    },
};
```

Userspace reads from `/sys/bus/iio/devices/iio:device0/`:

* `in_pressure_raw`: 97983 → 979.83 mbar
    
* `in_temp_raw`: 2073 → 20.73°C
    
* `in_humidityrelative_raw`: 3051 → 30.51%RH
    

## Testing

An automated test script handles module loading and sensor validation:

```bash
#!/bin/bash
make clean && make
sudo insmod main.ko

# Find IIO device
IIO_DEV=$(find /sys/bus/iio/devices/ -name "*ms8607*")

# Read sensors
echo "Pressure: $(cat $IIO_DEV/in_pressure_raw) (raw)"
echo "Temperature: $(cat $IIO_DEV/in_temp_raw) (raw)"
echo "Humidity: $(cat $IIO_DEV/in_humidityrelative_raw) (raw)"
```

All three sensors reported valid readings on hardware.

## CI/CD Pipeline

GitHub Actions runs static analysis and device tree validation on every push:

* **checkpatch**: Linux kernel coding style verification
    
* **sparse**: Static analyzer for kernel code
    
* **cppcheck**: Additional static analysis
    
* **Device tree compilation**: Validates overlay syntax
    

ARM cross-compilation builds run on manual trigger or release tags. The workflow builds `vmlinux` before modules to ensure proper symbol resolution—building only `make modules` caused missing symbol errors in `modpost`.

## Platform-Specific Issues

### ARM32 Division

The ARM32 kernel doesn't support 64-bit division. This issue showed up during CI cross-compilation—code that built fine on x86 failed with a link error on ARM:

```c
// WRONG - link error on ARM32
s64 temp = 2345;
dev_info(&client->dev, "Temp: %lld°C\n", temp / 100);
// ERROR: "__aeabi_ldivmod" undefined!
```

Fix: cast to 32-bit before division:

```c
// CORRECT
s64 temp = 2345;
int temp_int = (int)temp;
dev_info(&client->dev, "Temp: %d°C\n", temp_int / 100);
```

## Resources

**Hardware & Datasheets**:

* [MS8607 Datasheet](https://www.te.com/commerce/DocumentDelivery/DDEController?Action=srchrtrv&DocNm=MS8607-02BA01&DocType=DS&DocLang=English)
    
* [Adafruit MS8607 Breakout](https://www.adafruit.com/product/4716)
    
* [BeagleBone Black Documentation](https://docs.beagleboard.org/)
    

**Linux Kernel Documentation**:

* [IIO Subsystem](https://docs.kernel.org/driver-api/iio/)
    
* [I2C Subsystem](https://docs.kernel.org/i2c/writing-clients.html)
    
* [Device Tree Usage](https://docs.kernel.org/devicetree/usage-model.html)
    

**Code References**:

* [MS8607 Driver Repository](https://github.com/billvanleeuwen424/ms8607-driver)
    
* [MS5637 Driver](https://github.com/torvalds/linux/blob/master/drivers/iio/pressure/ms5637.c) (similar sensor)
    
* [BeagleBone Device Tree Overlays](https://github.com/beagleboard/bb.org-overlays)
    

**Learning Resources**:

* [Bootlin Training Materials](https://bootlin.com/training/)
    
* [Bootlin Device Tree 101](https://www.youtube.com/watch?v=Nz6aBffv-Ek)
    

---

This project gave me hands-on experience with the driver layer of embedded Linux—something I haven't yet had the chance to work with. Writing a kernel module from scratch meant learning more about device trees, IIO subsystem internals, I2C protocol details, and platform-specific constraints like ARM32 arithmetic limitations.

If you're interested in embedded Linux development, the full source code and documentation are available in the [GitHub repository](https://github.com/billvanleeuwen424/ms8607-driver).

Thanks for reading! :)
