Appearance
6. Peripherie via hwmon und iio
Übersicht zu Komponenten und Versionen
Name | Version | Link |
---|---|---|
Ubuntu | 22.04 LTS | link |
Raspberry Pi OS | Raspberry Pi OS Lite | link |
Linux Kernel | rpi-6.6.y | link |
ARM gcc Toolchain | 13.2.Rel1 | link |
Virtualbox | 7.0.18 | link |
Buildroot | 2024.02.2 | link |
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! - ... wieder zurück in der Menuconfig suchen wir nach
SHT4x
: ... und stellen fest, dass der Treiber bereits mitm
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):
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.
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.