Skip to content

6. Peripherie via hwmon und iio

Übersicht zu Komponenten und Versionen

NameVersionLink
Ubuntu22.04 LTSlink
Raspberry Pi OSRaspberry Pi OS Litelink
Linux Kernelrpi-6.6.ylink
ARM gcc Toolchain13.2.Rel1link
Virtualbox7.0.18link
Buildroot2024.02.2link

I2C mit Temperatur / Humidity Sensor SHT40

Der SHT40 ist ein beliebter Sensor um Temperatur und Luftfeuchtigkeit zu messen. Via I2C werden relevante Daten vom SoC ausgelesen und interpretiert.

Hierzu ist es natürlich möglich einen Userspace Treiber zu schreiben, welcher über das I2C Device File System auf den I2C Bus zugreift und entsprechende Read und Write Operationen durchführt.

In C und Python würde hierzu die smbus Library verwendet werden.

Kernel Device Tree und Overlay

Die Peripherie des Raspberry Pi wird mit dem Device Tree Blob, kurz .dtb, beschrieben. Das zugehörige File findet man in der Bootpartition unter dem Namen bcm2711-rpi-4-b.dtb. Das passende Source File findet sich im Kernel unter ./arch/arm/boot/dts/broadcom/bcm2711-rpi-4-b.dts.

Der Inhalt des .dts source files beschreibt die Peripherie und Register, welche dem Raspberry Pi angelegt sind. Hier exemplarisch der Inhalt des Files:

./arch/arm/boot/dts/broadcom/bcm2711-rpi-4-b.dts
bash
// SPDX-License-Identifier: GPL-2.0
/dts-v1/;
#define BCM2711
#define i2c0 i2c0if
#include "bcm2711.dtsi"
#include "bcm283x-rpi-wifi-bt.dtsi"
#undef i2c0
#include "bcm270x.dtsi"
#define i2c0 i2c0mux
#include "bcm2711-rpi.dtsi"
#undef i2c0
#include "bcm283x-rpi-led-deprecated.dtsi"
//#include "bcm283x-rpi-usb-peripheral.dtsi"

/ {
	compatible = "raspberrypi,4-model-b", "brcm,bcm2711";
	model = "Raspberry Pi 4 Model B";

	chosen {
		/* 8250 auxiliary UART instead of pl011 */
		stdout-path = "serial1:115200n8";
	};

	sd_io_1v8_reg: regulator-sd-io-1v8 {
		compatible = "regulator-gpio";
		regulator-name = "vdd-sd-io";
		regulator-min-microvolt = <1800000>;
		regulator-max-microvolt = <3300000>;
		regulator-boot-on;
		regulator-always-on;
		regulator-settling-time-us = <5000>;
		gpios = <&expgpio 4 GPIO_ACTIVE_HIGH>;
		states = <1800000 0x1>,
			 <3300000 0x0>;
		status = "okay";
	};

	sd_vcc_reg: regulator-sd-vcc {
		compatible = "regulator-fixed";
		regulator-name = "vcc-sd";
		regulator-min-microvolt = <3300000>;
		regulator-max-microvolt = <3300000>;
		regulator-boot-on;
		enable-active-high;
		gpio = <&expgpio 6 GPIO_ACTIVE_HIGH>;
	};
};

&bt {
	shutdown-gpios = <&expgpio 0 GPIO_ACTIVE_HIGH>;
};

&ddc0 {
	status = "okay";
};

&ddc1 {
	status = "okay";
};

&expgpio {
	gpio-line-names = "BT_ON",		/*  0 */
			  "WL_ON",
			  "PWR_LED_OFF",
			  "GLOBAL_RESET",
			  "VDD_SD_IO_SEL",
			  "CAM_GPIO",		/*  5 */
			  "SD_PWR_ON",
			  "SD_OC_N";
};

&gpio {
	/*
	 * Parts taken from rpi_SCH_4b_4p0_reduced.pdf and
	 * the official GPU firmware DT blob.
	 *
	 * Legend:
	 * "FOO" = GPIO line named "FOO" on the schematic
	 * "FOO_N" = GPIO line named "FOO" on schematic, active low
	 */
	gpio-line-names = "ID_SDA",		/*  0 */
			  "ID_SCL",
			  "GPIO2",
			  "GPIO3",
			  "GPIO4",
			  "GPIO5",		/*  5 */
			  "GPIO6",
			  "GPIO7",
			  "GPIO8",
			  "GPIO9",
			  "GPIO10",		/* 10 */
			  "GPIO11",
			  "GPIO12",
			  "GPIO13",
			  /* Serial port */
			  "GPIO14",
			  "GPIO15",		/* 15 */
			  "GPIO16",
			  "GPIO17",
			  "GPIO18",
			  "GPIO19",
			  "GPIO20",		/* 20 */
			  "GPIO21",
			  "GPIO22",
			  "GPIO23",
			  "GPIO24",
			  "GPIO25",		/* 25 */
			  "GPIO26",
			  "GPIO27",
			  "RGMII_MDIO",
			  "RGMIO_MDC",
			  /* Used by BT module */
			  "CTS0",		/* 30 */
			  "RTS0",
			  "TXD0",
			  "RXD0",
			  /* Used by Wifi */
			  "SD1_CLK",
			  "SD1_CMD",		/* 35 */
			  "SD1_DATA0",
			  "SD1_DATA1",
			  "SD1_DATA2",
			  "SD1_DATA3",
			  /* Shared with SPI flash */
			  "PWM0_MISO",		/* 40 */
			  "PWM1_MOSI",
			  "STATUS_LED_G_CLK",
			  "SPIFLASH_CE_N",
			  "SDA0",
			  "SCL0",		/* 45 */
			  "RGMII_RXCLK",
			  "RGMII_RXCTL",
			  "RGMII_RXD0",
			  "RGMII_RXD1",
			  "RGMII_RXD2",		/* 50 */
			  "RGMII_RXD3",
			  "RGMII_TXCLK",
			  "RGMII_TXCTL",
			  "RGMII_TXD0",
			  "RGMII_TXD1",		/* 55 */
			  "RGMII_TXD2",
			  "RGMII_TXD3";
};

&hdmi0 {
	status = "okay";
};

&hdmi1 {
	status = "okay";
};

&led_act {
	gpios = <&gpio 42 GPIO_ACTIVE_HIGH>;
};

&leds {
	led_pwr: led-pwr {
		label = "PWR";
		gpios = <&expgpio 2 GPIO_ACTIVE_LOW>;
		default-state = "keep";
		linux,default-trigger = "default-on";
	};
};

&pixelvalve0 {
	status = "okay";
};

&pixelvalve1 {
	status = "okay";
};

&pixelvalve2 {
	status = "okay";
};

&pixelvalve4 {
	status = "okay";
};

&pwm1 {
	pinctrl-names = "default";
	pinctrl-0 = <&pwm1_0_gpio40 &pwm1_1_gpio41>;
	status = "okay";
};

/* EMMC2 is used to drive the SD card */
&emmc2 {
	vqmmc-supply = <&sd_io_1v8_reg>;
	vmmc-supply = <&sd_vcc_reg>;
	broken-cd;
	status = "okay";
};

&genet {
	phy-handle = <&phy1>;
	phy-mode = "rgmii-rxid";
	status = "okay";
};

&genet_mdio {
	phy1: ethernet-phy@1 {
		/* No PHY interrupt */
		reg = <0x1>;
	};
};

&pcie0 {
	pci@0,0 {
		device_type = "pci";
		#address-cells = <3>;
		#size-cells = <2>;
		ranges;

		reg = <0 0 0 0 0>;

		usb@0,0 {
			reg = <0 0 0 0 0>;
			resets = <&reset RASPBERRYPI_FIRMWARE_RESET_ID_USB>;
		};
	};
};

/* uart0 communicates with the BT module */
&uart0 {
	pinctrl-names = "default";
	pinctrl-0 = <&uart0_ctsrts_gpio30 &uart0_gpio32>;
	uart-has-rtscts;
};

/* uart1 is mapped to the pin header */
&uart1 {
	pinctrl-names = "default";
	pinctrl-0 = <&uart1_gpio14>;
	status = "okay";
};

&vc4 {
	status = "okay";
};

&vec {
	status = "disabled";
};

&wifi_pwrseq {
	reset-gpios = <&expgpio 1 GPIO_ACTIVE_LOW>;
};

// =============================================
// Downstream rpi- changes

#include "bcm271x-rpi-bt.dtsi"

/ {
	soc {
		/delete-node/ pixelvalve@7e807000;
		/delete-node/ hdmi@7e902000;
	};
};

#include "bcm2711-rpi-ds.dtsi"
#include "bcm283x-rpi-csi1-2lane.dtsi"
#include "bcm283x-rpi-i2c0mux_0_44.dtsi"

/ {
	chosen {
		bootargs = "coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_headphones=0";
	};

	/delete-node/ wifi-pwrseq;
};

&mmcnr {
	pinctrl-names = "default";
	pinctrl-0 = <&sdio_pins>;
	bus-width = <4>;
	status = "okay";
};

&uart0 {
	pinctrl-0 = <&uart0_pins &bt_pins>;
	status = "okay";
};

&uart1 {
	pinctrl-0 = <&uart1_pins>;
};

&spi0 {
	pinctrl-names = "default";
	pinctrl-0 = <&spi0_pins &spi0_cs_pins>;
	cs-gpios = <&gpio 8 1>, <&gpio 7 1>;

	spidev0: spidev@0{
		compatible = "spidev";
		reg = <0>;	/* CE0 */
		#address-cells = <1>;
		#size-cells = <0>;
		spi-max-frequency = <125000000>;
	};

	spidev1: spidev@1{
		compatible = "spidev";
		reg = <1>;	/* CE1 */
		#address-cells = <1>;
		#size-cells = <0>;
		spi-max-frequency = <125000000>;
	};
};

&gpio {
	gpio-line-names = "ID_SDA",
			  "ID_SCL",
			  "GPIO2",
			  "GPIO3",
			  "GPIO4",
			  "GPIO5",
			  "GPIO6",
			  "GPIO7",
			  "GPIO8",
			  "GPIO9",
			  "GPIO10",
			  "GPIO11",
			  "GPIO12",
			  "GPIO13",
			  "GPIO14",
			  "GPIO15",
			  "GPIO16",
			  "GPIO17",
			  "GPIO18",
			  "GPIO19",
			  "GPIO20",
			  "GPIO21",
			  "GPIO22",
			  "GPIO23",
			  "GPIO24",
			  "GPIO25",
			  "GPIO26",
			  "GPIO27",
			  "RGMII_MDIO",
			  "RGMIO_MDC",
			  /* Used by BT module */
			  "CTS0",		/* 30 */
			  "RTS0",
			  "TXD0",
			  "RXD0",
			  /* Used by Wifi */
			  "SD1_CLK",
			  "SD1_CMD",		/* 35 */
			  "SD1_DATA0",
			  "SD1_DATA1",
			  "SD1_DATA2",
			  "SD1_DATA3",
			  /* Shared with SPI flash */
			  "PWM0_MISO",		/* 40 */
			  "PWM1_MOSI",
			  "STATUS_LED_G_CLK",
			  "SPIFLASH_CE_N",
			  "SDA0",
			  "SCL0",		/* 45 */
			  "RGMII_RXCLK",
			  "RGMII_RXCTL",
			  "RGMII_RXD0",
			  "RGMII_RXD1",
			  "RGMII_RXD2",		/* 50 */
			  "RGMII_RXD3",
			  "RGMII_TXCLK",
			  "RGMII_TXCTL",
			  "RGMII_TXD0",
			  "RGMII_TXD1",		/* 55 */
			  "RGMII_TXD2",
			  "RGMII_TXD3";

	bt_pins: bt_pins {
		brcm,pins = "-"; // non-empty to keep btuart happy, //4 = 0
				 // to fool pinctrl
		brcm,function = <0>;
		brcm,pull = <2>;
	};

	uart0_pins: uart0_pins {
		brcm,pins = <32 33>;
		brcm,function = <BCM2835_FSEL_ALT3>;
		brcm,pull = <0 2>;
	};

	uart1_pins: uart1_pins {
		brcm,pins;
		brcm,function;
		brcm,pull;
	};

	uart1_bt_pins: uart1_bt_pins {
		brcm,pins = <32 33 30 31>;
		brcm,function = <BCM2835_FSEL_ALT5>; /* alt5=UART1 */
		brcm,pull = <0 2 2 0>;
	};
};

&i2c0if {
	clock-frequency = <100000>;
};

&i2c1 {
	pinctrl-names = "default";
	pinctrl-0 = <&i2c1_pins>;
	clock-frequency = <100000>;
};

&i2s {
	pinctrl-names = "default";
	pinctrl-0 = <&i2s_pins>;
};

// =============================================
// Board specific stuff here

&sdhost {
	status = "disabled";
};

&phy1 {
	led-modes = <0x00 0x08>; /* link/activity link */
};

&gpio {
	audio_pins: audio_pins {
		brcm,pins = <40 41>;
		brcm,function = <4>;
		brcm,pull = <0>;
	};
};

&led_act {
	default-state = "off";
	linux,default-trigger = "mmc0";
};

&led_pwr {
	default-state = "off";
};

&pwm1 {
	status = "disabled";
};

&vchiq {
	pinctrl-names = "default";
	pinctrl-0 = <&audio_pins>;
};

&cam1_reg {
	gpio = <&expgpio 5 GPIO_ACTIVE_HIGH>;
};

cam0_reg: &cam_dummy_reg {
};

i2c_csi_dsi0: &i2c0 {
};

/ {
	__overrides__ {
		audio = <&chosen>,"bootargs{on='snd_bcm2835.enable_headphones=1 snd_bcm2835.enable_hdmi=1',off='snd_bcm2835.enable_headphones=0 snd_bcm2835.enable_hdmi=0'}";

		act_led_gpio = <&led_act>,"gpios:4";
		act_led_activelow = <&led_act>,"gpios:8";
		act_led_trigger = <&led_act>,"linux,default-trigger";

		pwr_led_gpio = <&led_pwr>,"gpios:4";
		pwr_led_activelow = <&led_pwr>,"gpios:8";
		pwr_led_trigger = <&led_pwr>,"linux,default-trigger";

		eth_led0 = <&phy1>,"led-modes:0";
		eth_led1 = <&phy1>,"led-modes:4";
	};
};

Device Tree erweitern

Natürlich wäre es möglich, den Device Tree zu erweitern um zum Beispiel einen neuen Sensor via I2C oder SPI anzuschliessen.

Es gibt für den Zweck von solchen Erweiterungen aber einen anderen Mechnismus, welche es zur Laufzeit ermöglicht neue Peripherie dynamisch hinzuzufügen und so mehr Modularität zu erzielen und nicht bestehende komplexe .dts Files direkt zu manipulieren.

Mit Device Tree Overlays können stellen des Device Trees überschrieben werden, oder neue Peripherie in die Baumstruktur eingefügt werden.

Auch hier gibt es unterschiedliche Wege, welche zum Ziel führen.

Beim Raspberry Pi werden Device Tree Overlays häufig direkt beim Booten geladen. In config.txt spezifiziert man, welches Overlay File .dtbo geladen werden soll.

Device Tree Overlays laden

Eine Lister aller Overlays, welche für den Kernel kompiliert worden Sind erhalten Sie, wenn Sie folgenden find Command im Kernel Source verzeichnis ausführen.

bash
find  arch/arm/boot/dts/overlays/ -name "*.dtbo"

Alle verfügbaren Overlays, welche mit dem Kernel fürs Raspberry Pi kompiliert worden sind, landen in der Boot Partition unter dem Verzeichnis overlays. Verifizieren Sie, das am besten gleich.

Neben vielen Raspberry Pi Hats werden auch eine Reihe an Sensoren unterstützt.

Als Helfer für I2C Sensoren findet man im Raspberry Pi Kernel den Overlay i2c-sensor.

Lesen Sie die Dokumentation hierzu im Kernel Source, welche Sie finden unter /arch/arm/boot/dts/overlays/README.

Neben einer Liste von Unterstützten Sensoren finden Sie auch eine Anleitung um diese beim Booten direkt zu laden.

Für den SHT40 heisst das in diesem Fall, dass wir config.txt um folgende Zeilen erweitern:

bash
dtparam=i2c=on
dtoverlay=i2c-sensor,sht4x

Die Dokumentation ist in diesem Fall verwirrend, das Sie die Syntax dtoverlay=i2c-sensor,<param>=<val> beschreibt. dtparam=i2c=on führt dazu, dass das Modul i2c-bcm2835 und i2c-dev beim booten des userspaces geladen wird.

Überprüfen Sie das mit lsmod.

und insmod

Via modprobe können Sie Kerneltreiber (.ko) Files zur Laufzeit laden. Hierbei reicht es den Treibernamen zu verwenden (i2c-bcm2835). Das Modul wird anhand der Verzeichnisstruktur in /lib/modules/ automatisch gesucht.

insmod lädt auch Kernel Treiber, allerdings übergeben Sie hierbei den Pfad zum .ko File.

Kernel Treiber für SHT40

Es reicht nicht, die Peripherie im dtb bloss zu beschreiben. Auch muss der entsprechende Treiber für den SHT40 im Kernel kompiliert werden.

Hierzu suchen wir den Treiber in der menuconfig des Kernels.

bash
export ARCH=arm
export CROSS_COMPILE=<toolchain-prefix>
make menuconfig
  • Suchen Sie mit / in der Menuconfig nach SHT40.
  • ... leider haben wir kein Glück, es scheint keinen Treiber zu geben.
  • Versuchen Sie SHT40 im Kernel in den Sourcen zu finden ...
  • grep -sir sht40
  • ... es scheint ein Config Flag dafür vorhanden zu sein: drivers/hwmon/Kconfig: If you say yes here you get support for the Sensiron SHT40, SHT41 and
  • öffnen wir das File, sehen wir, dass sich die Config hinter SHT4x verbirgt! sht4x
  • ... wieder zurück in der Menuconfig suchen wir nach SHT4x: sht4x ... und stellen fest, dass der Treiber bereits mit m als Modul selektiert ist.
  • Der Treiber wird hier offensichtlich von hwmon geladen, welcher über /sys/class/hwmon zugänglich ist.

Ein neues Builden des Kernels oder der Module ist also nicht notwendig, das der Treiber bereits beim letzten mal Kompilieren selektiert war.

Sensor am I2C Bus anschliessen

Den Sensor können wir nun am I2C Bus 1 anschliessen (Pins 3,5) und mit 3.3V / GND speisen (Pin 1, Pin 9):

pinout

Kerneltreiber laden

In der Rootshell des Targets können wir den Treiber also versuchen zu laden.

bash
# zur Sicherheit, i2c laden
modprobe i2c-bcm2835
modprobe i2c-dev
modprobe sht4x

Sensor nicht richtig angeschlossen

Falls der Sensor nicht richtig angeschlossen wurde, wird das laden des Moduls im dmesg folgenden Fehler:

bash
# [   90.131483] sht4x: probe of 1-0044 failed with error -5

Entfernen Sie den Treiber nochmals mit rmmod sht4x und rmmod crc8 und stellen Sie sicher, dass die Pins richtig verbunden sind.

Hat das geklappt, dann sollten wir neu unter /sys/class/hwmon den Sensor hwmon1 sehen.

Quiz: Um was handelt es sich wohl bei hwmon0?

Das ist der interne Temperatursensor vom Raspberry Pi SoC.

Zeigen wir die Files im Verzeichnis /sys/class/hwmon/hwmon1 an, stellen wir fest, dass wir zwei Files temp1_input und humidity1_input vom Treiber zur Verfügung gestellt erhalten. Mit cat können wir Werte auslesen und auf dem Terminal ausgeben.

Das File update_interval legt dabei fest, was das Zeitinterval einer neuen Messung ist (default 2000 Millisekunden). Mit echo 5000 > update_interval können wir die Messwerte alle 5 Sekunden erneuern.

Minimales Pollinterval

Im Source File ./drivers/hwmon/sht4x.c ist das minimale Pollinterval auf 2 Sekunden gesetzt. Wünschen Sie häufigere Updates, dann können Sie den Wert des define SHT4x_MIN_POLL_INTERVAL ändern.

c
// SPDX-License-Identifier: GPL-2.0-only

/*
 * Copyright (c) Linumiz 2021
 *
 * sht4x.c - Linux hwmon driver for SHT4x Temperature and Humidity sensor
 *
 * Author: Navin Sankar Velliangiri <navin@linumiz.com>
 */

#include <linux/crc8.h>
#include <linux/delay.h>
#include <linux/hwmon.h>
#include <linux/i2c.h>
#include <linux/jiffies.h>
#include <linux/module.h>

/*
 * Poll intervals (in milliseconds)
 */
#define SHT4X_MIN_POLL_INTERVAL	2000

/*
 * I2C command delays (in microseconds)
 */
#define SHT4X_MEAS_DELAY_HPM	8200	/* see t_MEAS,h in datasheet */
#define SHT4X_DELAY_EXTRA	10000

Nach der Änderung ist es Notwendig die Kernelmodule nochmals neu zu kompilieren mit make modules.

Mit dem Sysfs Treiber von hwmon ist es nun so relativ einfach möglich via Character Devices Messwerte des Sensors auszulesen. An dieser Stelle sei erwähnt, dass wir hierzu keinen eigenen Devicetree/Overlay erstellt haben, sondern mit dem existierenden Overlay i2c-sensor gearbeitet haben.

Overlay zur Laufzeit laden

Es ist möglich solche dtbo overlays auch zur Laufzeit ohne config.txt zu laden (hier nur exemplarisch für einen overlay names sensor.dtbo).

bash

# configfs mounten
mount -t configfs none /sys/kernel/config

# Verzeichnis für overlay erstellen
mkdir -p /sys/kernel/config/device-tree/overlays/my-sensor

# dtbo laden
cat sensor.dtbo > /sys/kernel/config/device-tree/overlays/my-sensor/dtbo

Pressure Sensor bmp280 mit iio

Ein anderes Beispiel bietet der BMP280 Sensor, welcher wie der SHT40 über I2C angesteuert werden kann.

Auch hier bietet sich der gleiche i2c-sensor Overlay an. Im README ./arch/arm/boot/dts/overlays/README finden wir folgenden Eintrag:

bash
bmp280                  Select the Bosch Sensortronic BMP280
						Valid addresses 0x76-0x77, default 0x76

dtoverlay

In config.txt hinterlegen wir also neu

bash
dtparam=i2c=on
dtoverlay=i2c-sensor,bmp280

Kerneltreiber

In der Menuconfig des Kernels stellen wir sicher, dass der BMP280 als Modul erstellt wird.

bash
# Im Kernel Source
export ARCH=arm
export CROSS_COMPILE=<toolchain-prefix>
make menuconfig

Suchen Sie im Kernel nach dem BMP280.

bmp280

Support für beides, i2c und spi scheint bereits vorhanden zu sein.

Module laden

Starten Sie das Raspberry Pi neu und verbinden Sie die I2C pins mit dem Sensor (SDA und SCL), wie beim SHT40.

Laden Sie der Reihe nach folgende Module:

bash
modprobe i2c-bcm2835
modprobe i2c-dev
modprobe bmp280
modprobe bmp280-i2c

Werte auslesen

Neu sollten Sie ein Characterdevice unter /sys/bus/iio/devices/iio:device0 erhalten. Das File in_pressure_input gibt hierbei den aktuellen Luftdruck zurück.