--- author: email: mail@petermolnar.net image: https://petermolnar.net/favicon.jpg name: Peter Molnar url: https://petermolnar.net copies: - http://web.archive.org/web/20190624125741/https://petermolnar.net/linux-i2c-iio-collectd/ lang: en published: '2018-07-13T21:00:00+00:00' summary: A short story of getting a tiny, cheap USB I²C adapter for a home server, learning about the Industrial I/O linux subsystem, and connecting it to collectd. tags: - home automation title: Using I²C sensors on a linux via a USB and IIO --- **Notes: no warranties. This is hardware, so it can cause trouble with your system, especially if you short-circuit something or - as I did once, many moons ago - solder on the fly why the thing is still connected to the USB port. Don't do that.** ![Proto-assembly of Digispark ATTiny85, Adafruit BME280, and Adafruit SI1145](digispark-attiny85-bme280-si1145.jpg) ## USB I²C adapter A few months ago I wrote about using a Raspberry Pi with some I²C sensors to collect data for Collectd[^1]. While it worked well, it made me realise that having the RPi running a full fledged operating system means I need to apply security patches to yet another machine, and that is not something I want to deal with. I also have a former laptop, running as a ZFS based NAS, so why not use that? After venturing into a fruitless dig to use the I²C port in the VGA connector[^2] I verified that indeed, as concluded in the tutorial, it doesn't work with embedded Intel graphics on linux. Alternative I started looking at USB I²C adapter, but they are expensive. There is one project though, which looked very promising, and it didn't require a full-fledged Arduino either: Till Harbaum's I²C-Tiny-USB[^3]. It uses an ATtiny85 board - as the name suggests, it's tiny, and turned out to be a perfectly fine USB to I²C adapter. You can buy one here: *Note: there's an Adafruit FT232H, which, in theory, is capable of the same thing. I haven't tested it.* ### I2C-Tiny-USB firmware The git repository already contains a built `hex` file, but in case there are any modifications needed to be done, this is how it's done: ``` {.bash} sudo -i apt install gcc-avr avr-libc cd /usr/src git clone https://github.com/harbaum/I2C-Tiny-USB cd I2C-Tiny-USB/digispark make hex ``` Make sure the `I2C_IS_AN_OPEN_COLLECTOR_BUS` is uncommented; I've tried with real pull-up resistors, and, for my surprise, the sensors stopped showing up. ### micronucleus flash utility To flash the hex file, you'll need `micronucleus`, a tiny flasher utility. ``` {.bash} sudo -i apt install libusb-dev cd /usr/src git clone https://github.com/micronucleus/micronucleus cd micronucleus/commandline make CONFIG=t85_default make install ``` Run: ``` {.bash} micronucleus --run --dump-progress --type intel-hex main.hex ``` then connect the device through a USB port, and wait for the end of the flash process. ## I²C on linux Surprisingly enough, Debian did not show I²C hubs in `/dev` - apparently the kernel module for this is not loaded, so load it, and make that load permanent: ``` {.bash} sudo -i modprobe i2c-dev echo "i2c-dev" >> /etc/modules ``` ### Connect the Attiny85 Normally a PC already has a serious amount of I²C adapters. As a result, the new device will show up with an extra device number, which number is rather important. The kernel log can help identify that: ``` {.ḃash} dmesg | grep i2c-tiny-usb [ 3.721200] usb 5-2: Product: i2c-tiny-usb [ 3.725693] i2c-tiny-usb 5-2:1.0: version 2.01 found at bus 005 address 003 [ 3.736109] i2c i2c-1: connected i2c-tiny-usb device [ 3.736584] usbcore: registered new interface driver i2c-tiny-usb ``` To read just the device number: ``` {.bash} i2cdev=$(dmesg | grep 'connected i2c-tiny-usb device' | head -n1 | sed -r 's/.*\s+i2c-([0-9]+).*/\1/') ``` **Note: the device number might change after a reboot. For me, it was `10` when simply plugged in, and `1` if it was connected during a reboot.** ### Detecting I2C devices `i2cdetect` is a program that dumps all the devices responding on an I²C adapter. The Adafruit website has a collection for their sensors[^4]. That `1` after the `i2cdetect -y` is the device number identified in the previous step, and it says I have 2 devices: ``` {.bash} sudo -i i2cdev=$(dmesg | grep 'connected i2c-tiny-usb device' | head -n1 | sed -r 's/.*\s+i2c-([0-9]+).*/\1/') i2cdetect -y ${i2cdev} 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: 60 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- 77 ``` ## I²C 0x77: BME280 temperature, pressure, humidity sensor[^5] {#ic-0x77-bme280-temperature-pressure-humidity-sensor1} This is where things got interesting. Normally, when a `BME280` sensors comes into play, every tutorial starts pulling out Python for the task, given that most of the Adafruit libraries are in Python. Don't get me wrong, those are great libs, and the Python solutions are decent, but doing a `pip3 search bme280` resulted in this: bme280 (0.5) - Python Driver for the BME280 Temperature/Pressure/Humidity Sensor from Bosch. Adafruit-BME280 (1.0.1) - Python code to use the BME280 temperature/humidity/pressure sensor with a Raspberry Pi or BeagleBone black. adafruit-circuitpython-bme280 (2.0.2) - CircuitPython library for the Bosch BME280 temperature/humidity/pressure sensor. bme280_exporter (0.1.0) - Prometheus exporter for the Bosh BME280 sensor RPi.bme280 (0.2.2) - A library to drive a Bosch BME280 temperature, humidity, pressure sensor over I2C Which one to use? Then there are the dependencies, and the code quality varies from one to another. So I started digging into the internet, github, and other sources, and somehow I realised there's a kernel module, named `bmp280`. The `BMP280` is a sibling of the `BME280` - it's without the humidity sensor. So the questions was: what in the world is `drivers/iio/pressure/bmp280-i2c.c` and how can I use it? It turned out, that apart from `hwmon`, there's another sensor library layer in the linux kernel, called Industrial I/O - iio. It was added with this name somewhere in 2012, around 3.15[^6], and it's purpose is to offer a subsystem fast speed sensors[^7]. While fast speed is not a thing for me this time, but I do trust the kernel code quality. For my greatest surprise, the `BMP280` module is even included in the Debian Sid kernel as a module, and adding it was a mere: ``` {.bash} sudo -i modprobe bmp280 echo "bmp280" >> /etc/modules modprobe bmp280-i2c echo "bmp280-i2c" >> /etc/modules ``` To actually enable the device, the i2c bus has to be told of the sensor's existence: ``` {.bash} sudo -i i2cdev=$(dmesg | grep 'connected i2c-tiny-usb device' | head -n1 | sed -r 's/.*\s+i2c-([0-9]+).*/\1/') echo "bme280 0x77" > /sys/bus/i2c/devices/i2c-${i2cdev}/new_device ``` The `kernel` log should show something like this: kernel: bmp280 1-0077: 1-0077 supply vddd not found, using dummy regulator kernel: bmp280 1-0077: 1-0077 supply vdda not found, using dummy regulator kernel: i2c i2c-1: new_device: Instantiated device bme280 at 0x77 Verify the device is working: ``` {.bash} tree /sys/bus/iio/devices/iio\:device0 /sys/bus/iio/devices/iio:device0 ├── dev ├── in_humidityrelative_input ├── in_humidityrelative_oversampling_ratio ├── in_pressure_input ├── in_pressure_oversampling_ratio ├── in_pressure_oversampling_ratio_available ├── in_temp_input ├── in_temp_oversampling_ratio ├── in_temp_oversampling_ratio_available ├── name ├── power │   ├── async │   ├── autosuspend_delay_ms │   ├── control │   ├── runtime_active_kids │   ├── runtime_active_time │   ├── runtime_enabled │   ├── runtime_status │   ├── runtime_suspended_time │   └── runtime_usage ├── subsystem -> ../../../../../../../../../bus/iio └── uevent 2 directories, 20 files ``` And that's it. The `BME280` is ready to be used: ``` {.bash} for f in in_pressure_input in_temp_input in_humidityrelative_input; do echo "$f: $(cat /sys/bus/iio/devices/iio\:device0/$f)"; done in_pressure_input: 102.112671875 in_temp_input: 26050 in_humidityrelative_input: 49.611328125 ``` According to the `BME280` datasheet[^8], under recommended modes of operation (3.5.1 Weather monitoring), the oversampling for each sensor should be 1, so: ``` {.bash} sudo -i echo 1 > /sys/bus/iio/devices/iio\:device0/in_pressure_oversampling_ratio echo 1 > /sys/bus/iio/devices/iio\:device0/in_temp_oversampling_ratio echo 1 > /sys/bus/iio/devices/iio\:device0/in_humidityrelative_oversampling_ratio ``` ## I²C 0x60: SI1145 UV index, light, IR sensor[^9] {#ic-0x60-si1145-uv-index-light-ir-sensor2} Unlike the BME280, the SI1145 doesn't have a built-in kernel module in Debian Sid - but it does exist as a kernel module, it's simply not included in the Debian Kernel. I've also learnt that this sensor is a heavyweight player, and that I should have bought something way simpler for mere light measurements; something that's already included the out-of-the-box kernel modules, like a TSL2561[^10]. But I wasn't willing to give up the SI1145, being an expensie sensor, so in order to have it in the kernel, I had to compile the kernel module. Before getting started make sure: - your system is up to date - you have rebooted since the last kernel update Once those two are true, identify the kernel version: ``` {.bash} uname -a Linux system-hostname 4.17.0-1-amd64 #1 SMP Debian 4.17.3-1 (2018-07-02) x86_64 GNU/Linux ``` The output contains `4.17.3-1` - **that is the actual kernel version**, not the `4.17.0-1-amd64` which is the Debian name. Get the kernel; extract it; add the SI1145 to the config; compile the `drivers/iio/light` modules; add that to the local modules. ``` {.bash} sudo -i cd /usr/src/ wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.17.3.tar.gz tar xf linux-4.17.3.tar.gz cd linux-4.17.3 cp /boot/config-4.17.0-1-amd64 .config cp ../linux-headers-4.17.0-1-amd64/Module.symvers . echo "CONFIG_SI1145=m" >> .config make menuconfig # save it # exit make prepare make modules_prepare make SUBDIRS=scripts/mod make M=drivers/iio/light SUBDIRS=drivers/iio/light modules cp drivers/iio/light/si1145.ko /lib/modules/$(uname -r)/kernel/drivers/iio/light/ depmod modprobe si1145 echo "si1145" >> /etc/modules ``` Once that is done, and there are no error messages, enable the device: ``` {.bash} sudo -i i2cdev=$(dmesg | grep 'connected i2c-tiny-usb device' | head -n1 | sed -r 's/.*\s+i2c-([0-9]+).*/\1/') echo "si1145 0x60" > /sys/bus/i2c/devices/i2c-${i2cdev}/new_device ``` The `kernel` log shoud show something like this: kernel: si1145 1-0060: device ID part 0x45 rev 0x0 seq 0x8 kernel: si1145 1-0060: no irq, using polling kernel: i2c i2c-1: new_device: Instantiated device si1145 at 0x60 Verify the device is working: ``` {.bash} tree /sys/bus/iio/devices/iio\:device1 /sys/bus/iio/devices/iio:device1 ├── buffer │   ├── data_available │   ├── enable │   ├── length │   └── watermark ├── current_timestamp_clock ├── dev ├── in_intensity_ir_offset ├── in_intensity_ir_raw ├── in_intensity_ir_scale ├── in_intensity_ir_scale_available ├── in_intensity_offset ├── in_intensity_raw ├── in_intensity_scale ├── in_intensity_scale_available ├── in_proximity0_raw ├── in_proximity_offset ├── in_proximity_scale ├── in_proximity_scale_available ├── in_temp_offset ├── in_temp_raw ├── in_temp_scale ├── in_uvindex_raw ├── in_uvindex_scale ├── in_voltage_raw ├── name ├── out_current0_raw ├── power │   ├── async │   ├── autosuspend_delay_ms │   ├── control │   ├── runtime_active_kids │   ├── runtime_active_time │   ├── runtime_enabled │   ├── runtime_status │   ├── runtime_suspended_time │   └── runtime_usage ├── sampling_frequency ├── scan_elements │   ├── in_intensity_en │   ├── in_intensity_index │   ├── in_intensity_ir_en │   ├── in_intensity_ir_index │   ├── in_intensity_ir_type │   ├── in_intensity_type │   ├── in_proximity0_en │   ├── in_proximity0_index │   ├── in_proximity0_type │   ├── in_temp_en │   ├── in_temp_index │   ├── in_temp_type │   ├── in_timestamp_en │   ├── in_timestamp_index │   ├── in_timestamp_type │   ├── in_uvindex_en │   ├── in_uvindex_index │   ├── in_uvindex_type │   ├── in_voltage_en │   ├── in_voltage_index │   └── in_voltage_type ├── subsystem -> ../../../../../../../../../bus/iio ├── trigger │   └── current_trigger └── uevent 5 directories, 59 files ``` **Note: I tried, others tried, but even though in theory, there's a temperature sensor on the SI1145, it doesn't work. It seems like it reads the value on startup, and that's it.** ## CLI script In order to have a quick view, without collectd, or other dependencies, a script like this is more, than sufficient: ``` {.bash} #!/usr/bin/env bash d="$(date)" temperature=$(echo "scale=2;$(cat /sys/bus/iio/devices/iio\:device0/in_temp_input)/1000" | bc) pressure=$(echo "scale=2;$(cat /sys/bus/iio/devices/iio\:device0/in_pressure_input)*10/1" | bc) humidity=$(echo "scale=2;$(cat /sys/bus/iio/devices/iio\:device0/in_humidityrelative_input)/1" | bc) light_vis=$(cat /sys/bus/iio/devices/iio\:device1/in_intensity_raw) light_ir=$(cat /sys/bus/iio/devices/iio\:device1/in_intensity_ir_raw) light_uv=$(cat /sys/bus/iio/devices/iio\:device1/in_uvindex_raw) echo "$(hostname -f) $d Temperature: $temperature °C Pressure: $pressure mBar Humidity: $humidity % Visible light: $light_vis lm IR light: $light_ir lm UV light: $light_uv lm" ``` The output: your.hostname Thu Jul 12 08:48:40 BST 2018 Temperature: 25.59 °C Pressure: 1021.65 mBar Humidity: 49.28 % Visible light: 287 lm IR light: 334 lm UV light: 12 lm *Note: I'm not completely certain that the light unit is actually in lumens; the documentation is a bit fuzzy about that, so I assumed it is.* ## Collectd The next step is to actually collect the sensor readouts from the sensors. I'm still using `collectd`[^11], a small, ancient, yet stable and very good little metrics collection system, because it's enough. It writes ordinary `rrd` files, which can be plotted into graphs with tools like Collectd Graph Panel[^12] Unfortunately there's not yet an iio plugin for collectd (or I couldn't find it yet, and if you did, please let me know), so I had to add an extremely simple shell script as an exec plugin to collectd. `/usr/local/lib/collectd/iio.sh` ``` {.bash} #!/usr/bin/env bash HOSTNAME="${COLLECTD_HOSTNAME:-$(hostname -f)}" INTERVAL="${COLLECTD_INTERVAL:-60}" # this will run only on collectd load, and once it's loaded, # even though it throws and error, additional runs don't make any # problems i2cdev=$(dmesg | grep 'connected i2c-tiny-usb device' | head -n1 | sed -r 's/.*\s+i2c-([0-9]+).*/\1/') echo "bme280 0x77" > /sys/bus/i2c/devices/i2c-${i2cdev}/new_device echo "si1145 0x60" > /sys/bus/i2c/devices/i2c-${i2cdev}/new_device while true; do for sensor in /sys/bus/iio/devices/iio\:device*; do name=$(cat "${sensor}/name") if [ "$name" == "bme280" ]; then # unit: °C temp=$(echo "scale=2;$(cat ${sensor}/in_temp_input)/1000" | bc ) echo "PUTVAL $HOSTNAME/sensors-$name/temperature-temperature interval=$INTERVAL N:${temp}" # unit: mBar pressure=$(echo "scale=2;$(cat ${sensor}/in_pressure_input)*10/1" | bc) echo "PUTVAL $HOSTNAME/sensors-$name/pressure-pressure interval=$INTERVAL N:${pressure}" # unit: % humidity=$(echo "scale=2;$(cat ${sensor}/in_humidityrelative_input)/1" | bc) echo "PUTVAL $HOSTNAME/sensors-$name/percent-humidity interval=$INTERVAL N:${humidity}" elif [ "$name" == "si1145" ]; then # unit: lumen? ir=$(cat ${sensor}/in_intensity_ir_raw) echo "PUTVAL $HOSTNAME/sensors-$name/gauge-ir interval=$INTERVAL N:${ir}" light=$(cat ${sensor}/in_intensity_raw) echo "PUTVAL $HOSTNAME/sensors-$name/gauge-light interval=$INTERVAL N:${light}" uv=$(cat ${sensor}/in_uvindex_raw) echo "PUTVAL $HOSTNAME/sensors-$name/gauge-uv interval=$INTERVAL N:${uv}" fi done sleep "$INTERVAL" done ``` `/etc/collectd/collectd.conf` [...] LoadPlugin "exec" Exec "nobody" "/usr/local/lib/collectd/iio.sh" [...] The results are: ![BME280 temperature graph in Collectd Graph Panel](iio_bme280_temperature.png) ![SI1145 raw light measurement in Collectd Graph Panel](iio_si1145_light.png) ## Conclusions The Industrial I/O layer is something I've heard for the first time, but it's extremely promising: the code is clean, it already has support for a lot of sensors, and it seems to be possible to extend at a relative easy. Unfortunately it's documentation it brief and I'm yet to find any metrics collector that supports it out of the box, but that doesn't mean there won't be any very soon. Currently I'm very happy with my budget I2C USB solution - not having to run a Raspberry Pi for simple metrics collection is certainly in win, and utilising the sensors directly from the kernel also looks very decent. [^1]: [^2]: [^3]: [^4]: [^5]: [^6]: [^7]: [^8]: [^9]: [^10]: [^11]: [^12]: