Kampis Elektroecke

Frequenzmessung auf dem Raspberry Pi

In diesem Artikel zeige ich, wie ein Kernelmodul zur Frequenzmessung für den Raspberry entworfen und genutzt werden kann. Das Kernelmodul erzeugt ein virtuelles Gerät, welches anschließend mit einem Python-Programm ausgelesen wird. Der Rückgabewert des Gerätes ist die gemessene Frequenz des Signals. 

Aufbau des Kernelmoduls:

Die Zeiterfassung für das Messen der Signalfrequenz erfolgt im Kernelmodul mit Hilfe von Kernelinterrupts. Dazu wird der benötigte I/O als Ausgang geschaltet und ein Falling-Edge-Interrupt aktiviert:

static uint32_t ConfigGPIO(int Pin)
{
	uint32_t Error, IRQ_Number;
	char Name[20];

	snprintf(Name, sizeof(Name), "rpi-gpio-%d", Pin);
	Error = gpio_request(Pin, Name);
	if (Error) 
	{
		return -1;
	}

	Error = gpio_direction_input(Pin);
	if (Error) 
	{
		gpio_free(Pin);
		return -1;
	}

	IRQ_Number = gpio_to_irq(Pin);
	if (IRQ_Number < 0) 
	{
		gpio_free(Pin);
		return -1;
	}

	Error = request_irq(IRQ_Number, GPIO_ISR, IRQF_TRIGGER_FALLING, DEVICE_IRQ, DriverObject);
	if (Error) 
	{
		gpio_free(Pin);
		return -1;
	}

	return IRQ_Number;
}

Der Rückgabewert der Funktion ist eine Nummer, die auf den reservierten Interrupt verweist. Diese Nummer muss auf einen gültigen Wert überprüft und gespeichert werden, da sie beim Beenden des Moduls wieder freigegeben werden muss.

Bei jedem Sprung in die GPIO IRQ wird die aktuell gespeicherte Kernelzeit von der Variable Low in die Variable High kopiert und eine neue Zeit in Low gespeichert. 

static irqreturn_t GPIO_IRQ(int IRQ, void* Data)
{
	High = Low;
	ktime_get_ts(&Low);

	return IRQ_HANDLED;
}

Beide Werte werden beim Lesen des virtuellen Gerätes genutzt um, unter Berücksichtigung der Resolution, die Periodendauer zu berechnen und anschließend an den Userspace weiterzugeben.

static ssize_t DriverRead(struct file* Instanz, char __user* User, size_t ToCopy, loff_t* Offset)
{
	ssize_t NotCopied, BytesToCopy;
	unsigned long Mult;
	unsigned long Div;

	if(strcmp(Resolution, "ms\n") == 0)
	{
		Mult = 1000;
		Div = 1000000;
	}
	else if(strcmp(Resolution, "us\n") == 0)
	{
		Mult = 1000000;
		Div = 1000;
	}
	else
	{
		Mult = 1000000000;
		Div = 1;
	}

	High_us = (High.tv_sec * Mult) + (High.tv_nsec / Div);
	Low_us = (Low.tv_sec * Mult) + (Low.tv_nsec / Div);

	if(High_us > Low_us)
	{
		Diff_us = High_us - Low_us;
	}
	else
	{
		Diff_us = Low_us - High_us;
	}

	snprintf(Buffer, BUFFERSIZE, "%lu\n", Diff_us);
	BytesToCopy = min(ToCopy, sizeof(Buffer));
	if((NotCopied = copy_to_user(User, &Buffer, BytesToCopy)))
	{
		printk("Driver was not able to copy %lu byte\n", (unsigned long)NotCopied);
		return -EFAULT;
	}

	*Offset += BytesToCopy - NotCopied;

	return BytesToCopy - NotCopied;
}

Durch die If-Abfrage werden zudem ungültige Eingaben in der Resolution-Datei abgefangen. Die Auflösung wird in diesem Fall auf Nanosekunden gesetzt.

Über entsprechende Operationen im Proc-Filesystem wird die Konfiguration der Auflösung behandelt:

ssize_t ProcRead(struct file* Instanz, char __user* User, size_t ToCopy, loff_t* Offset)
{
	ssize_t NotCopied, BytesToCopy;

	if(ProcReadCounter == 0)
	{
		ProcReadCounter++;

		BytesToCopy = min(ToCopy, sizeof(Resolution));
		if((NotCopied = copy_to_user(User, &Resolution, BytesToCopy)))
		{
			printk("Driver was not able to copy %lu byte\n", (unsigned long)NotCopied);
			return -EFAULT;
		}

		*Offset += BytesToCopy - NotCopied;

		return BytesToCopy - NotCopied;
	}

	return 0;
}

ssize_t ProcWrite(struct file* Instanz, const char __user* User, size_t ToWrite, loff_t* Offset)
{
	ssize_t ToCopy, NotCopied;

	ToCopy = min(ToWrite, sizeof(Resolution));

	if(ToCopy == 3)
	{
		NotCopied = copy_from_user(Resolution, User, ToCopy);

		if(NotCopied == 0)
		{
			printk("Set new resolution to: %s", Resolution);
		}

		return ToCopy - NotCopied;
	}

	return ToCopy;
}

Damit sind die wichtigen Komponenten des Kernelmoduls erklärt. Das Modul muss jetzt noch kompiliert werden. Hierfür werden die Sourcen des verwendeten Kernels und eine passende Toolchain benötigt. Die Version des Kernels kann innerhalb der Konsole des Raspberry Pi abgerufen werden:

root@Raspberry:~# uname -r
4.14.52-v7+

Für die Kompilierung des Moduls kann das folgende Makefile genutzt werden:

obj-m += PeriodeCounter.o

# Current dir
PWD  := $(shell pwd)

# Kernel sources
KDIR := ${Raspberry_Kernel}

all:
	make ARCH=arm CROSS_COMPILE=$(Raspberry)- -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

Zudem sollten die Umgebungsvariablen für den Cross-Compiler und die Kernel-Sourcen gesetzt und der Kernel mind. einmal kompiliert worden sein. Wie das geht wird hier beschrieben.

Nachdem das Modul kompiliert worden ist, kann es auf den Raspberry Pi übertragen werden. Im nächsten Schritt schauen wir uns an, wie sich das Modul im Praxistest schlägt…

Test des Kernelmoduls:

Zum Testen habe ich die Hardware-PWM des Raspberry Pi verwendet. Die PWM wird im Standardfall an GPIO 18 ausgegeben und das Frequenzzählmodul verwendet GPIO 17. Beide Pins wurden miteinander gebrückt um die PWM mit dem Frequenzzähler auswerten zu können. Es muss dann nur noch die PWM eingestellt und aktiviert werden:

$ cd /sys/class/pwm/pwmchip0
$ echo 0 > export
$ echo 10000000 > pwm0/period
$ echo 8000000 > pwm0/duty_cycle
$ echo 1 > pwm0/enable

Anschließend wird das Kernelmodul geladen:

$ insmod PeriodeCounter.ko

Unter /dev ist nun ein neues Gerät mit dem Namen PeriodeCounter zu finden.

$ ls /dev

PeriodeCounter   loop3               ram4       tty18  tty44      uinput
...

Aus diesem Gerät kann die gemessene Signalfrequenz ausgelesen werden. Zusätzlich ist unter /proc/PeriodeCounter eine Datei namens Resolution zu finden. Mit dieser Datei kann die Auflösung des Frequenzzählers konfiguriert werden. Dabei stehen folgende Auflösungen zur Verfügung:

  • Milisekunden – ms
  • Mikrosekunden – us
  • Nanosekunden – ns

Das Auslesen der gemessenen Periodendauer kann in Python über einen Dateizugriff umgesetzt werden:

#!/usr/bin/python3

import os
import time

if(__name__ == "__main__"):
	with open("/proc/PeriodeCounter/Resolution", "r") as File:
		Resolution = File.readline().strip()
		print("[INFO] Resultion set to '{}'".format(Resolution))

	while(True):
		try:
			with open("/dev/PeriodeCounter", "r") as File:
				print("[INFO] Periode: {} {}".format(File.readline().strip(), Resolution))
			time.sleep(1)

		except KeyboardInterrupt:
			print("[INFO] Exit programm...")

Das Programm ließt die eingestellte Auflösung aus der Resolution-Datei aus. Anschließend wird in einer Endlosschleife das virtuelle Gerät des Frequenzzählers ausgelesen und das Ergebnis wird, zusammen mit der Auflösung, in der Konsole dargestellt.

root@Raspberry:/home/pi/Desktop/ReadPeriode# python ReadPeriode.py
[INFO] Resultion set to 'us'
[INFO] Periode: 10001 us
[INFO] Periode: 10000 us
root@Raspberry:/home/pi/Desktop/ReadPeriode# echo ms > /proc/PeriodeCounter/Relution
root@Raspberry:/home/pi/Desktop/ReadPeriode# python ReadPeriode.py              
[INFO] Resultion set to 'ms'
[INFO] Periode: 10 ms
[INFO] Periode: 10 ms

Das komplette Projekt kann in meinem Raspberry Pi GitLab-Repository gefunden und heruntergeladen werden.

Viele Grüße
Daniel

Ein Kommentar

Schreibe einen Kommentar zu Joey Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.