Wolumen Bitcoina

W ostatnim czasie temat kryptowalut, w tym Bitcoina, przycichł. Wiadomo, nikt nie chce ładować pieniędzy w aktywo, które straciło mocno na wartości w ostatnich miesiącach choć może właśnie teraz jest okno czasowe na zainteresowanie się tym tematem. Niemniej, kryptowaluty w tym Bitcoin jest to ciekawym obiektem do eksperymentów i analizy bez względu na zawirowania rynkowe.

Wolumen

Na rynkach kapitałowych wolumenem nazywa się ilość jednostek danego aktywa, które zmieniło w danym okresie właściciela. Przykładowo jeśli osoba O1 sprzedała osobie O2 100 akcji spółki S w danym dniu i była to jedyna transakcja na tej spółce w tym dniu to oznacza, że tego dnia wolumen wynosił 100.

Do tej pory wolumen był możliwy do ustalenia na rynkach scentralizowanych takich jak np. Giełda Papierów Wartościowych w Warszawie ponieważ notowane na niej spółki są w większości notowane tylko na tej jednej giełdzie zatem łatwo ustalić ile akcji danej spółki zmieniło właściciela w danym okresie. Nawet jeśli jakaś spółka jest notowana na więcej niż jednej giełdzie(jak np. UniCredit) to też nie ma większych problemów aby ustalić wolumen ponieważ giełd prowadzących handel danymi akcjami będzie co najwyżej kilka.

Problemem jest natomiast ustalenie wolumenu w przypadku rynków zdecentralizowanych jak np. rynek surowcowy albo walutowy(Forex). Na tych rynkach nie możemy w prosty sposób ustalić wolumenu ponieważ ilość ośrodków prowadzących sprzedaż/wymianę tych aktywów jest ogromna, a część transakcji może nie być nigdzie rejestrowana.

Wymiana/handel kryptowalut z założenia również jest zdecentralizowana, ale posiadają one jeden ciekawy aspekt- publiczny blockchain, który umożliwia prześledzenie wszystkich transakcji wykonanych w danej kryptowalucie, a co za tym idzie- możemy ustalić jaki był wolumen.

Blockchain

W internecie istnieje wiele opracowań co to jest więc nie będę się tutaj rozwodził nad tym temat. Natomiast jeśli nie wiesz co to jest to w tym artykule możemy uznać, że jest to taka baza danych, w której są przechowywane informacje o wszystkich transakcjach.

Jak sprawdzić wolumen?

Oczywiście można skorzystać ze stron z notowaniami, od końcówki 2021 roku wiele tego typu stron podaje również wolumen Bitcoina, a nie tylko jego aktualny kurs. Po drugie, można przejrzeć największe giełdy, sprawdzić jakie na nich są wolumeny i zsumować wszystko, w ten sposób również otrzymamy przybliżoną wartość wolumenu. Ale my jesteśmy bardziej ambitni i będziemy analizować blockchain.

Każdy blok w blockchainie Bitcoina ma oczywiście taką samą strukturę. To jak wygląda przykładowy blok można zobaczyć tutaj. Pod podanym adresem można znaleźć dane przedstawione w formacie json, oczywiście blockchain nie składa się plików jsonowych, ale nam zdecydowanie będzie prościej użyć takiej formy danych. Zainteresowani mogą sobie przejrzeć jakie dane są przechowywane, nas w tym przypadku interesują dwa pola- time czyli czas o której został utworzony dany blok oraz tx czyli transakcje.

Pole time interesuje nas aby móc określić kiedy nastąpiły dane transakcje, a pole tx nas interesuje ponieważ w nim znajdują się wielkości poszczególnych transakcji, a zatem to dzięki nim możemy zmierzyć wolumen.

Implementacja

Nie będziemy oczywiście sprawdzać każdego bloku oddzielnie i sumować danych na kalkulatorze, napiszemy prosty skrypt w Pythonie. Cały skrypt prezentuje się następująco:

import urllib.request, json
from datetime import datetime

daily_data = dict()
for block in range(754982, 754980, -1):
    try:
        with open("data/btc/{}.json".format(block)) as f:
            data = json.load(f)
    except:
        url = urllib.request.urlopen("https://blockchain.info/block-height/{}?format=json".format(block))
        data = json.loads(url.read().decode())
        with open("data/btc/{}.json".format(block), 'w') as f:
            json.dump(data, f)

    transactions = data["blocks"][0]["tx"]
    out_sum = 0
    in_sum = 0

    for t in transactions[1:]:
        for i in t["inputs"]:
            in_sum += i["prev_out"]["value"] / 10**8
        for o in t["out"]:
            out_sum += o["value"] / 10**8

    date = datetime.fromtimestamp(data["blocks"][0]["time"])
    if date.date() not in daily_data:
        daily_data[date.date()] = {"in_sum": 0, "out_sum": 0}
    daily_data[date.date()]["out_sum"] += out_sum
    daily_data[date.date()]["in_sum"] += in_sum

print(daily_data)

A teraz zajmijmy się poszczególnymi fragmentami kodu:

daily_data = dict()

Jest to słownik, w którym będą przechowywane dane w poszczególnych dniach.

for block in range(754982, 754000, -1):
    try:
        with open("data/btc/{}.json".format(block)) as f:
            data = json.load(f)
    except:
        url = urllib.request.urlopen("https://blockchain.info/block-height/{}?format=json".format(block))
        data = json.loads(url.read().decode())
        with open("data/btc/{}.json".format(block), 'w') as f:
            json.dump(data, f)

Iterujemy po blokach, pętla zaczyna od najnowszego do najstarszego bloku(oczywiście użyj aktualnych numerów bloków, te były nowe w momencie pisania artykułu). Następnie jest sprawdzane czy dane z danego bloku znajdują się w pamięci komputera, jeśli ich nie ma to zostaną one pobrane.

    transactions = data["blocks"][0]["tx"]
    out_sum = 0
    in_sum = 0

Dla prostoty słownik z transakcjami przypisujemy do nowej zmiennej. Zmienne out_sum oraz in_sum będą zawierać ilość zbywanych i nabywanych Bitcoinów.

for t in transactions[1:]:
        for i in t["inputs"]:
            in_sum += i["prev_out"]["value"] / 10**8
        for o in t["out"]:
            out_sum += o["value"] / 10**8

Przechodzimy przez kolejne transakcje i sumujemy zbywane i nabywane bitcoiny. Wartości te dzielimy przez 108 ponieważ są one przechowywane w jednostce satoshi czyli 10-8 Bitcoina. Pomijamy zerowy indeks ponieważ, o ile dobrze rozumiem, to w nim zawiera się premia dla górników czyli nowowygenerowane Bitcoiny. Czyli Bitcoiny, które nie brały udziału w transakcjach.


    date = datetime.fromtimestamp(data["blocks"][0]["time"])
    if date.date() not in daily_data:
        daily_data[date.date()] = {"in_sum": 0, "out_sum": 0}
    daily_data[date.date()]["out_sum"] += out_sum
    daily_data[date.date()]["in_sum"] += in_sum

Jeśli nie istnieje pole w słowniku odpowiadające dacie z aktualnego bloku to takowe jest tworzone, zawiera ono ilość zbywanych i nabywanych bitcoinów w danym dniu.

print(daily_data)

No i na koniec wyświetlamy zebrane dane.

Podczas odczytywania danych zauważysz, że ilość zbywanych Bitcoinów jest większa niż nabywanych. Wynika to z faktu, że w sieci Bitcoina istnieją opłaty transakcyjne.

W dalszych krokach można się potrudzić o bardziej szczegółową analizę transakcji np. czy więcej się przeciętnie sprzedaje czy więcej kupuje.

Zaszufladkowano do kategorii Analiza danych, Artykuły | Dodaj komentarz

Pomiar czasu użycia komputera

W poprzednim wpisie opisałem jak ograniczyć czasowo dostęp do wybranych stron, a w tym opiszę jak można śledzić czas spędzony przed komputerem. Koncepcja tego oprogramowania jest bardzo prosta, przed wyłączeniem komputera będzie wywoływany skrypt, który sprawdzał czas przez jaki był uruchomiony system i zapisywał odpowiednie dane do bazy danych.

Baza danych

Pierwszym elementem naszej układanki jest baza danych. Wykorzystamy bazę sqlite3. Baza będzię bardzo prosta. Będzię się ona składał jedynie z jednej tabeli zawierającej dwie kolumny- datę i czas użycia komputera w danym dniu. Bazę można utworzyć za pomocą poniższego skryptu:

import sqlite3
from datetime import date

conn = sqlite3.connect('/home/twójużytkownik/.time_monitor/time_monitor.db')
c = conn.cursor()
c.execute('CREATE TABLE IF NOT EXISTS time ([date] TEXT PRIMARY KEY, time REAL)')
conn.commit()

conn.close()

Zwróć uwagę na ścieżkę do bazy, ustaw ją tak aby odpowiadała twojemu użytkownikowi.

Skrypt

Czas, który minął od uruchomienia komputera w Linuksie można znaleźć w pliku /proc/uptime. Trzeba odczytać pierwszą wartość. Wartość ta jest wyrażona w sekundach.

Dla ciekawskich, druga wartość w tym pliku oznacza czas spędzony “na biegu jałowym”(w sensie na idle’u) przez wszystkie rdzenie procesora zatem jeśli mamy conajmniej dwurdzeniowy procesor(ma ktoś jeszcze jednordzeniowe procesory w ogóle?) to ta wartość będzie najprawdopodobniej większa niż czas pracy komputera.

Kolejnym elementem jest skrypt aktualizujący bazę danych. Jest on bardzo prosty. Działanie skryptu można opisać w następujących punktach:

  1. Odczyt czasu pracy systemu i zamiana odczytanej wartości na minuty
  2. Podłączenie się do bazy danych i sprawdzenie czy istnieje rekord z aktualną datą
  3. Jeśli nie istnieje to tworzymy taki wpis i zapisujemy zmierzony czas pracy systemu
  4. Jeśli istnieje to odczytujemy wcześniejszą i dodajemy do niej aktualny czas pracy systemu

Skrypt wygląda następująco:

#!/usr/bin/python3

import sqlite3
from datetime import date

with open("/proc/uptime", "r") as f:
	uptime = f.readline().split(' ')[0]

uptime = float(uptime) / 60
today = date.today()

conn = sqlite3.connect('/home/twójużytkownik/.time_monitor/time_monitor.db')
c = conn.cursor()
c.execute('SELECT * FROM time WHERE date="{}"'.format(today))
row = c.fetchall()
if len(row) == 0:
	c.execute('INSERT INTO time VALUES ("{}", {})'.format(today, uptime))
else:
	uptime += row[0][1]
	c.execute('UPDATE time SET time={} WHERE date="{}"'.format(uptime, today))
conn.commit()
conn.close()

Oczywiście popraw ścieżkę do bazy danych tak aby odpowiadała lokalizacji na twoim komputerze.

Skrypt ten powinien być uruchamiany przed wyłączeniem systemu. Podobnie jak w przypadku oprogramowania z poprzedniego wpisu użyjemy systemd. Skopiuj zatem powyższy skrypt do katalogu /etc. Następnie utwórz plik time_monitor.service w katalogu /etc/systemd/system. Plik time_monitor.service ma następującą zawartość:

[Unit]
Description=Time monitoring script
DefaultDependencies=no
Before=shutdown.target

[Service]
Type=oneshot
ExecStart=/etc/time_monitor.py
TimeoutStartSec=0

[Install]
WantedBy=shutdown.target

Aby aktywować działanie wykonaj komendę:

# Wykonaj jako sudo lub root
systemctl enable website_scheduler.service

Sprawdzenie statystyk

Sprawdzenie swoich statystyk będzie polegało na odczycie danych z bazy danych, Najprościej to zrobić za pomocą sqlite3:

sqlite3 ~/.time_monitor/time_monitor.db

W bazie wykonujemy taką instrukcję SQLową:

SELECT * FROM time;

Komenda ta wydrukuje na ekran całą tabelę time. Aby dostać np. dane z ostatnich 10 dni można użyć komendy:

SELECT * FROM time ORDER BY date DESC LIMIT 10;

Teraz możemy monitorować czas spędzony przed komputerem. Kolejnym krokiem może być przygotowanie skrypty generującego wykresy przedstawiające zgromadzone dane.

Zaszufladkowano do kategorii Artykuły, Linux | Dodaj komentarz

Czasowe ograniczenie dostępu do stron

Internet to wspaniały wynalazek, ale jednocześnie wiele rzeczy znajdujących się w nim może nas odciągać od zadań, które powinniśmy wykonać. Na rynku są dostępne narzędzia typu wtyczka do przeglądarki BlockSite, która umożliwia ustawienie ograniczenia czasowego dostępu do stron, ale te rozwiązania mają jedną wadę- są płatne. A jak mówi mądrość ludowa: jedynie darmo to uczciwa cena.

Zablokowanie dostępu do stron

Najprostszym sposobem na ograniczenie dostępu do strony w Linuksie jest modyfikacja pliku /etc/hosts. Aby ograniczyć dostęp do adresu twojastrona.pl należy do /etc/hosts dodać takie linijki:

127.0.1.1  twojastrona.pl
127.0.1.1  www.twojastrona.pl
127.0.1.1  http://twojastrona.pl
127.0.1.1  https://twojastrona.pl
127.0.1.1  http://www.twojastrona.pl
127.0.1.1  https://www.twojastrona.pl

Reszta pliku pozostaje niezmodyfikowana. W ten sposób można zablokować nieograniczoną ilość stron. Robimy aż 6 wpisów aby mieć pewność, że adres będzie rozwinięty do adresu wskazującego na komputer lokalny.

Czasowe ograniczenie dostępu

Wiemy jak zablokować strony na stałe, ale teraz jawi się pytanie jak utworzyć ograniczenie czasowe? Po pierwsze potrzebujemy dwóch plików /etc/hosts– jeden z listą stron, które chcemy blokować w danym czasie oraz drugi bez tych stron(czyli prawdopodobnie oryginalny /etc/hosts). Możemy je odpowiednio nazwać /etc/hosts.block oraz /etc/hosts.unblock.

Drugim elementem jest skrypt pythonowy, który będzie nadpisywać plik /etc/hosts jedną z wcześniej utworzonych kopii. W tym przykładzie założyłem, że strony są blokowane w godzinach pracy czyli od poniedziałku do piątku do godziny 15:00. Kod jest bardzo krótki i wygląda tak:

#!/usr/bin/python3

import datetime
import shutil

now = datetime.datetime.now()

if now.weekday() >= 0 and now.weekday() < 5:
    if now.hour < 15:
        shutil.copyfile("/etc/hosts.block", "/etc/hosts")
    elif now.hour >= 15:
        shutil.copyfile("/etc/hosts.unblock", "/etc/hosts")
else:
    shutil.copyfile("/etc/hosts.unblock", "/etc/hosts")

Zwróć uwagę, że wskazuję interpreter w pierwszej linii skryptu, jest to istotne ponieważ skrypt ten będzie używany przez systemd. Skrypt umieść w katalogu /etc i nadaj mu prawa wykonywania.

Teraz musimy tak skonfigurować systemd aby skrypt uruchamiał się w trakcie startowania systemu. Aby to zrobić utwórz plik website_scheduler.service o zawartości:

[Unit]
Description=Website scheduler

[Service]
ExecStart=/etc/website_scheduler.py

[Install]
WantedBy=multi-user.target

Umieść ten plik w katalogu /etc/systemd/system.

Teraz możemy aktywować nasz serwis za pomocą komendy:

# Wykonaj jako sudo lub root
systemctl enable website_scheduler.service

Niektórzy mogą zadać słuszne pytanie- “Ale co jeśli włącze komputer o 14:59? Wtedy podczas startu systemu skopiuje się z poblokowanymi stronami”. Jest to jak najbardziej słuszny zarzut. Aby obejść ten problem wykorzystamy crontaba.

Wywołaj komendę:

crontab -e

Jeśli pierwszy raz uruchamiasz crontaba zostaniesz zapytany o swój ulubionu konsolowy edytor tekstu, wybierz jakikolwiek Ci pasuje. W edytorze dodaj taką oto linijkę:

0 15 * * 1-5 /etc/website_scheduler.py

Ta linijka oznacza, że od poniedziałku do piątku o godzinie 15:00 będzie wywoływany nasz skrypt.

I to tyle. Skrypt można dostosować do swoich potrzeb aby odpowiadał godzinom, w których pracujesz. Być może warto pokusić się o dodanie obsługi pliku konfiguracyjnego.

I na koniec jedno spostrzeżenie- mając dostęp do użytkownika root to to ten ogranicznik jest oczywiście łatwy do obejścia. Zresztą jak każde inne narzędzie tego typu.

Zaszufladkowano do kategorii Artykuły, Linux | Dodaj komentarz

Regulator PID na Raspberry Pi 4

Na moich studiach z automatyki i robotyki dużo się mówiło o regulatorach wszelkiego typu, było dużo wykresów, dużo wzorów i trochę trudnych mądrych słów. Natomiast nikt nie zademonstrował jak zrobić taki regulator dlatego kilka lat od zakończenia studiów z automatyki postanowiłem w końcu zaspokoić moją ciekawość i sam przygotowałem regulator PID silnika elektrycznego. Za regulator posłużył mi popularny komputerek Raspberry Pi 4 ze skryptem napisanym w pythonie.

Co będzie potrzebne?

Potrzebne jest 5 rzeczy:

  • Raspberry Pi 4
  • Silnik elektryczny, ja wykorzystałem silnik firmy DFRobot o numerze katalogowym FIT0450
  • Mostek H, ja użyłem mostka L293D
  • Koszyczek na baterie AA aby móc zasilić silnik
  • Kabelki goldpin męsko-żeńskie

Wszystkie wymienione elementy można kupić w sklepie botland.

Teoria

Nie będę się rozwodzić na teorią, jest tego wystarczająco dużo w internecie, nie ma sensu mnożyć bytów ponad potrzebę. Dobre omówienie za co jest odpowiedzialny dany typ regulatora znajduje się w tym nagraniu:

Podłączenie

Podłączenie mostka H L293D wygląda następująco:

Schemat jest dosyć prosty. Zwrócę jedynie uwagę, że wszystkie piny GND powinny być ze sobą połączone pomimo faktu, że piny z lewej strony są podłączone do zasilania bateryjnego, a piny GND z prawej strony do RPi.

Podłączenie silnika z enkoderem wygląda tak:

Piny 1 i 2 to wyprowadzenia silnika. Piny 3 i 4 to sygnały enkodera. W tym przykładzie interesuje nas tylko określenie obrotów silnika dlatego jeden z pinów zostaje niepodłączony. Użycie dwóch sygnałów enkodera było by konieczne gdybyśmy chcieli określać kierunek obrotu silnika. Piny 5 i 6 to zasilanie enkodera.

Przygotowanie Raspberry Pi

Na Raspberry Pi mam zainstalowany system Raspberry Pi OS. Aby móc sterować prędkością silnika musimy włączyć obsługę PWM w systemie. Aby to zrobić w pliku config.txt znajdującym się na partycji boot musimy dodać następującą linijkę:

dtoverlay=pwm,pin=12

Powyższa linijka oznacza, że chcemy uruchomić wsparcie dla PWM na pinie GPIO12

Pomiar prędkości obrotowej

Użyty przeze mnie silnik posiada przekładnie o przełożeniu 120:1. Enkoder silnika generuje 16 impulsów na jeden pełny obrót. Mnożąc 120 przez 16 otrzymamy ilość impulsów, które generuje enkoder na jeden pełny obrót wału. Zatem otrzymujemy 1920 impulsów na jeden obrót wału.

Będziemy chcieli zmierzyć ilość obrotów wału na minutę czyli mierzymy tzw. RPM. Pomiar prędkości będzie dokonywany co sekundę. Czyli będziemy przeliczać ilość impulsów na sekundę na ilość obrotów na minutę.

Musimy rozwiązać następujący układ równań:

# Ilość impulsów na minutę to 1920 razy ilość obrotów na minutę
1920 * RPM = IPM
# Ilość impulsów na minutę to 60 razy ilość impulsów na sekundę
IPM = 60 * IPS

Mamy wyznaczone IPM czyli ilość impulsów na minutę w obu równaniach więc możemy je do siebie przyrównać:

1920 * RPM = 60 * IPS
RPM = (60 / 1920) * IPS
RPM = IPS / 32

Aby więc obliczyć aktualną prędkość musimy podzielić ilość impulsów silnika na sekundę przez 32.

Kod regulatora

Regulator napisałem w pythonie, cały skrypt ma dokładnie 64 linie licząc puste linijki. Omówmy po kolei każdy kawałek kodu:

import signal
import sys
import time
import threading
import RPi.GPIO as GPIO

Oczywiście skrypt zaczynamy od zaimportowania niezbędnych pakietów. Pakiet RPi umożliwia pracę z peryferiami podłączonymi do RPi, nie jest on chyba domyślnie zainstalowany więc trzeba to zrobić samemu.

INPUT1_GPIO = 23
INPUT2_GPIO = 24
ENABLE_GPIO = 12
ENCODER_GPIO = 4

Powyższe zmienne wskazują piny do których są podłączone endkoder i mostek H.

impulses = 0
mutex = threading.Lock()
set_value = int(sys.argv[1])

W zmiennej impulses będziemy przechowywać ilość wykrytych impulsów wygenerowanych przez enkoder, mutex będzie nam umożliwiaj sterowanie dostępem do zmiennej impulses. Zmienna set_value to wartość zadana czyli w naszym przypadku ilość obrotów wału silnika na minutę, wartość tę będziemy przekazywać jako parametr skryptu.

def signal_handler(sig, frame):
	GPIO.cleanup()
	sys.exit(0)

Duża część pracy skryptu będzie wykonywana w nieskończonej pętli, jedyną możliwością zakończenia jego pracy będzie wciśnięcie kombinacji Ctrl+C. Aby nie pozostawiać po sobie bałaganu zostanie wywołana ta funkcja gdy zostanie wciśnięta ta kombinacja.

def encoder_callback(channel):
	global impulses
	mutex.acquire()
	impulses += 1
	mutex.release()

Jest to funkcja, która będzie zliczać impulsy wygenerowane przez enkoder. Jak widać dostęp do zmiennej impulses regulujemy za pomocą wcześniej zdefiniowanego muteksa.

GPIO.setmode(GPIO.BCM)
GPIO.setup(INPUT1_GPIO, GPIO.OUT)
GPIO.setup(INPUT2_GPIO, GPIO.OUT)
GPIO.setup(ENABLE_GPIO, GPIO.OUT)
GPIO.setup(ENCODER_GPIO, GPIO.IN)
GPIO.add_event_detect(ENCODER_GPIO, GPIO.BOTH, callback=encoder_callback)

en_pwm = GPIO.PWM(ENABLE_GPIO, 1000)
en_pwm.start(0)

GPIO.output(INPUT1_GPIO, 1)
GPIO.output(INPUT2_GPIO, 0)

Powyższe linie to ustawienia pinów. Pierwsza linia GPIO.setmode określa sposób w jaki będziemy się odwoływać do poszczególnych pinów na płytce. GPIO.BCM oznacza, że będziemy się do nich odwoływać tak jak to jest zdefiniowane w dokumentacji Broadcoma.

Funkcja GPIO.add_event_detect umożliwia przypisanie funkcji do wywołania gdy nastąpi dane zdarzenie. W naszym przypdaku zdarzeniem jest zmiana stanu pinu.

signal.signal(signal.SIGINT, signal_handler)

Ustawiamy wywołanie funkcji signal_handler jako reakcję na naciśnięcie kombinacji klawiszy Ctrl+C.

kp = 0.3
ki = 0.2
kd = 0.1
prev_error = 0
err_sum = 0

Zmienne przechowujące nastawy regulatora, poprzedni błąd oraz sumę błędów. Poprzedni błąd będzie potrzebny aby obliczyć sygnał części różniczkującej, a suma błędów aby obliczyć sygnał części całkującej.

while True:
	mutex.acquire()
	error = set_value - (impulses / 32)
	err_sum += error
	P = kp * error
	I = ki * err_sum
	D = kd * (error - prev_error)

	duty = P + I + D
	if duty > 100.0:
		duty = 100.0
	elif duty < 0.0:
		duty = 0.0
	en_pwm.ChangeDutyCycle(duty)
	print(impulses / 32)
	impulses = 0
	prev_error = error
	mutex.release()
	time.sleep(1)

Pętla nieskończona w której są obliczane sygnały naszego regulatora. Wszystkie obliczenia są dokonywane po zamknięciu muteksa.

Najpierw obliczamy aktualny błąd. Zwróć uwagę, że wartość zadana jest wyrażona w RPM czyli nie możemy tak po prostu odjąć od niej zmierzonego sygnału, sygnał musi być przekształcony na tę samą jednosktę dlatego dzielimy impulsów przez 32.

Następnie zwiększamy sumę błędów, która to będzie użyta przez część całkującą.

Gdy już mamy obliczony błąd możemy przystąpić do obliczenia sygnałów poszczególnych części naszego regulatora. Kolejn obliczamy wartość sygnału części proporcjonalnej poprzez zwykłe pomnożenie błędu przez wartość kp. Wartość sygnału części całkującej obliczamy poprzez pomnożenie sumy błędów przez wartość ki. Ostatnią wartość czyli wyjście części różniczkującej obliczamy poprzez pomnożenie różnicy pomiędzy aktualnym błędem a poprzednim błędem przez wartość kd.

Gdy już mamy obliczone wartości wyjściowe wszystkich części to sumujemy je. Suma ta będzie wypełnieniem sygnału PWM. Wartość wypełnienia zawiera się w widełkach od 0.0 do 100.0 dlatego też jest w pętli warunek sprawdzający czy obliczona suma mieści się w tych widełkach. Następnie oczywiście ustawiamy tę wartość na pinie za pomocą ChangeDutyCycle.

Następnie wyświetlamy aktualną prędkość na ekranie. Po tych wszystkich operacjach możemy wyzerować ilość zliczonych impulsów. Zapisujemy wartość błędu do zmiennej prev_errror aby użyć jej w kolejnym przebiegu pętli. Zwalniamy muteksa i czekamy sekundę aby powtórzyć cały proces.

Regulator działa w miarę dobrze, regulacja do obrotów na poziomie ok.70-80 RPM w moim przypadku zachodziła dobrze. Regulacja dla 90 RPM działała delikatnie mówiąc tak sobie. A jak wygląda przy wyższych prędkościach to nie wiem ponieważ szybciej nie udało mi się rozpędzić silnika. Może podałem zbyt niskie zasilanie.

Jeśli chodzi o potencjalne rzeczy do poprawy to napewno trzeba by się przyjrzeć nastawom regulatora ponieważ te tak sobie z kapelusza wziąłem i nie szukałem za długo lepszych nastaw.

Zaszufladkowano do kategorii Artykuły, Programowanie ogólnie | Dodaj komentarz

Lekcja 15- Zakończenie

To byłoby na tyle z rzeczy, które chciałem przedstawić. W chwili obecnej posiadasz już sporo wiedzy nt. kernela Linuksa, umiesz implementować już proste moduły co jest dobrą podstawą do dalszej nauki jeśli interesuje Cię ta tematyka.

Polecane lektury

Tematyka kernela Linuksa jest cały czas dosyć niszowa choć wydaje mi się, że zyskuje ona na popularności w ostatnich, z tego powodu ciężko znaleźć polskojęzyczne źródło wiedzy na ten temat. W większości będę polecał lektury anglojęzyczne:

  1. Jądro Linuksa, Robert Love- książka omawiająca jak działa Linux od środka. Nie jest ona najnowsza, pozwala ona jednak zaznajomić się z najważniejszymi mechanizmami.
  2. Writing Linux Device Drivers, Jerry Cooperstein- nieco leciwa już bo wydana w 2009 roku książka omawiająca od podstaw implementację modułów linuksowych. Dla każdego zagadnienia są przygotowane zadania. Jest wydana również książeczka z rozwiązaniami tych zadań. Wg mnie jej główną bolączką jest brak omówienia device-tree, które nie było obsługiwane w Linuksie w momencie pisania tej książki.
  3. Linux Device Drivers Development, John Madieu- nieco nowsza pozycja od poprzedniej, książka ta została wydana w 2017 roku. Próg wejścia do tej książki jest w mojej opinii wyższy niż w przypadku Writing Linux Device Drivers, ale omawia ona wiele podsystemów jak SPI, IIO, RTC, Regmap API i wiele innych.
  4. Linux Device Driver Tutorial, embetronicx– obszerny kurs o modułach dostępny na stronie embetronicx.com za darmo(a to uczciwa cena).
  5. Linux Device Drivers, Greg Kroah-Hartman- książka z 2004 roku więc w świecie technologii jest to książka niemalże średniowieczna. Oczywiście można się z niej wiele dowiedzieć, w szczególności jeśli interesuje nas dogłębnie jakieś zagadnienie, nie pokrywa ona jednak wielu rzeczy, które są obecnie dostępne w Linuksie. Również próg wejścia do niej jest bardzo wysoki, sam przez nią nigdy nie przebrnąłem. Kilka lat temu widziałem, że ma książka ta ma mieć nowe czwarte wydanie jednak cały czas czekam na nie…

Jeśli nie żal Tobie pieniędzy na książki to polecam zapoznanie się z pozycjami z punktów 1, 2 oraz 3. Taką kolejność czytania też bym polecał.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 14- Górne i dolne połówki

Wstęp

W tej lekcji zapoznamy się z koncepcją górnych i dolnych połówek(ang. top and bottom halves). Pojęcie połówek ma związek z obsługą przerwań. Używanie połówek oznacza, że obsługa danego zdarzenia zostaje podzielona na dwie części. Pierwszą część będzie stanowić stanowić procedura obsługi przerwania znana nam z lekcji 6. Taka procedura powinna się wykonywać możliwie szybko, dlatego powinno się w niej wykonać czynności, które muszą być wykonane natychmiast po wystąpieniu danego zdarzenia takie jak np. pobranie nowootrzymanych danych przez jakiś moduł komunikacyjny. Następnie przerwanie powinno zakolejkować do wykonania kolejną procedurę, która wykona ewentualne przetwarzanie otrzymanych danych. Tę drugą procedurę, która może się wykonać później nazywamy właśnie dolną połówką.

Implementacja

W Linuksie koncepcje połówek można zaprogramować na kilka sposób, najczęściej prezentuje się użycie taskletów oraz workqueue. Różnica pomiędzy tymi mechanizmami jest bardzo techniczna, tasklet jest wykonywany jako przerwanie, natomiast workqueue jest wykonywane jako zwykły proces. Jakie ma to znaczenie dla nas? Przede wszystkim taskletu nie możemy uśpić, a workqueue możemy.

W tej lekcji zaimplementujemy zarówno użycie taskletu i workqueue. Będziemy bazować na kodzie z lekcji 6. Dla przypomnienia napisaliśmy tam moduł, który w reakcji na naciśnięcie podłączonego przycisku zapalał diodę. Teraz dodatkowo w przerwaniu zakolejkujemy tasklet lub workqueue, które to będą logować coś do logu systemowego.

Tasklet

Kod niewiele się różni od kodu z lekcji 6 i wygląda następująco:

#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>

#define LED 48		//BBB
#define BUTTON 49
//#define LED 23			//RPi4
//#define BUTTON 24

static int led_state = 0;
static int di;

void tasklet_fun(unsigned long arg)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_TASKLET(tasklet, tasklet_fun, 1);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	tasklet_schedule(&tasklet);
	return IRQ_HANDLED;
}

static int __init helloworld_init(void)
{
	int ret;

	if(!gpio_is_valid(LED)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", LED);
		ret = -ENODEV;
		goto err1;
	}
	gpio_request(LED, "sysfs");
	gpio_direction_output(LED, led_state);
	gpio_export(LED, false);

	if(!gpio_is_valid(BUTTON)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", BUTTON);
		ret = -ENODEV;
		goto err2;
	}
	gpio_request(BUTTON, "sysfs");
	gpio_direction_input(BUTTON);
	gpio_set_debounce(BUTTON, 200);
	gpio_export(BUTTON, false);

	if((ret = request_irq(gpio_to_irq(BUTTON), irq_handler, IRQF_TRIGGER_RISING, "interrupt_test", &di))) {
		pr_err("Cannot register IRQ, ret: %i\n", ret);
		goto err3;
	}

	pr_info("Init succeeded\n");
	return 0;

err3:
	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);
err2:
	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
err1:
	return ret;
}

static void __exit helloworld_exit(void)
{
	free_irq(gpio_to_irq(BUTTON), &di);

	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);

	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
	pr_info("Exit succeeded\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");

Większość kodu powinna być dla Ciebie zrozumiała, nas interesują następujące linijki:

void tasklet_fun(unsigned long arg)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_TASKLET(tasklet, tasklet_fun, 1);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	tasklet_schedule(&tasklet);
	return IRQ_HANDLED;
}

Idąc od góry, definiujemy funkcję dla naszego taskletu. Jest ona bardzo prosta, wypisuje komunikat do logu systemowego.

Następnie deklarujemy tasklet za pomocą makra DECALRE_TASKLET. Przyjmuje ono 3 parametry- sam tasklet, funkcję taskletu oraz domyślną wartość argumentu przekazywanego do funkcji taskletu. Argument ten może być później zmieniony poprzez zmianę wartości pola data w naszym tasklecie.

Ostatnim interesującym nas elementem jest wywołanie funkcji tasklet_schedule. Wywołanie tej funkcji kolejkuje nasz tasklet do wykonania w przyszłości.

Workqueue

Podobnie jak w przypadku taskletu i w przypadku workqueue kod niewiele różni się od kodu z lekcji 6:

#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/workqueue.h>

#define LED 48		//BBB
#define BUTTON 49
//#define LED 23			//RPi4
//#define BUTTON 24

static int led_state = 0;
static int di;

void workqueue_fun(struct work_struct *work)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_WORK(workqueue, workqueue_fun);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	schedule_work(&workqueue);
	return IRQ_HANDLED;
}

static int __init helloworld_init(void)
{
	int ret;

	if(!gpio_is_valid(LED)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", LED);
		ret = -ENODEV;
		goto err1;
	}
	gpio_request(LED, "sysfs");
	gpio_direction_output(LED, led_state);
	gpio_export(LED, false);

	if(!gpio_is_valid(BUTTON)) {
		pr_err("Invalid GPIO pin(%i)!!!\n", BUTTON);
		ret = -ENODEV;
		goto err2;
	}
	gpio_request(BUTTON, "sysfs");
	gpio_direction_input(BUTTON);
	gpio_set_debounce(BUTTON, 200);
	gpio_export(BUTTON, false);

	if((ret = request_irq(gpio_to_irq(BUTTON), irq_handler, IRQF_TRIGGER_RISING, "interrupt_test", &di))) {
		pr_err("Cannot register IRQ, ret: %i\n", ret);
		goto err3;
	}

	pr_info("Init succeeded\n");
	return 0;

err3:
	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);
err2:
	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
err1:
	return ret;
}

static void __exit helloworld_exit(void)
{
	free_irq(gpio_to_irq(BUTTON), &di);

	gpio_set_value(BUTTON, 0);
	gpio_unexport(BUTTON);
	gpio_free(BUTTON);

	gpio_set_value(LED, 0);
	gpio_unexport(LED);
	gpio_free(LED);
	pr_info("Exit succeeded\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");

W tym przypadku interesują nas następujące linijki:

void workqueue_fun(struct work_struct *work)
{
	pr_info("Printing is too slow for interrupt\n");
}

DECLARE_WORK(workqueue, workqueue_fun);

static irqreturn_t irq_handler(int irq, void *dev_id)
{
	led_state ^= 1;
	gpio_set_value(LED, led_state);
	schedule_work(&workqueue);
	return IRQ_HANDLED;
}

Zaczynamy od funkcji dla naszego workqueue, która jest wypisuje tylko komunikat do logu systemowego.

Następnie deklarujemy nasze workqueue za pomocą makra DECLARE_WORK, które przyjmuje dwa parametry- workqueue i przypisaną do niej funkcje.

Ostatnim elementem układanki jest wywołanie funkcji schedule_work.Wywołanie tej funkcji kolejkuje nasze workqueue do wykonania w przyszłości.

Kompilacja

No to jeszcze szybki makefile:

obj-m += 14_tasklet.o 14_workqueue.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

Przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp. Testy zacznijmy od taskletu. Załaduj moduł na swojej płytce:

sudo insmod 14_tasklet.ko

Wciśnij przycisk podłączony do twojej płytki. Dioda powinna zmienić swój stan. Sprawdź czy odpowiedni komunikat pojawił się w logu systemowym:

dmesg

Aby przetestować workqueue odładuj moduł z taskletem:

sudo rmmod 14_tasklet

I wykonaj analogiczny test z workqueue:

sudo insmod 14_workqueue.ko
# wciśnij przycisk
dmesg
Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 13- I2C

Wstęp

W tej lekcji zapoznamy się z obsługą magistrali I2C z poziomu modułu kernelowego. Wykorzystamy tutaj wiedzę z poprzednich lekcji- a w szczególności z lekcji o sterownikach platformowych i device-tree.

Implementacja

W tej lekcji zaimplementujemy sterownik dla zegara RTC DS3231. Na pierwszy rzut oka kod wydaje się być całkiem długi, ma on ponad 400 linii, ale funkcje, które czytają lub zapisują poszczególne atrybuty są w gruncie rzeczy do siebie bardzo podobne dlatego też nie będziemy szczegółowo omawiać każdej z nich. Implementacja wygląda następująco:

#include <linux/module.h>
#include <linux/i2c.h>

#define UPPER_NIBBLE(x) ((x & 0xF0) >> 4)
#define LOWER_NIBBLE(x) (x & 0x0F)

/* DS3231 register defines */
#define SECOND_REG 0x00
#define MINUTE_REG 0x01
#define HOUR_REG 0x02
#define DAY_REG 0x03
#define DATE_REG 0x04
#define MONTH_REG 0x05
#define YEAR_REG 0x06

/* write functions */
static void write_reg(struct i2c_client *client, char reg, char val)
{
	char buf_wr[2];
	struct i2c_msg msg[] =
	{
		{
			.addr = client->addr,
			.flags = 0x00,
			.len = 2,
			.buf = buf_wr,
		},
	};
	buf_wr[0] = reg;
	buf_wr[1] = val;
	i2c_transfer(client->adapter, msg, 1);
}

static char read_reg(struct i2c_client *client, char reg)
{
	char buf_rd[1];
	char buf_wr[1] = {reg};
	struct i2c_msg msg[] =
	{
		{
			.addr = client->addr,
			.flags = 0x00,
			.len = 1,
			.buf = buf_wr,
		},
		{
			.addr = client->addr,
			.flags = I2C_M_RD,
			.len = 1,
			.buf = buf_rd,
		},
	};
	i2c_transfer(client->adapter, msg, 2);
	return buf_rd[0];
}

static ssize_t sec_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, SECOND_REG));
}

static ssize_t sec_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int sec;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &sec);
	if(UPPER_NIBBLE(sec) > 5 || LOWER_NIBBLE(sec) > 9)
		return -EINVAL;

	write_reg(client, SECOND_REG, (char)sec);
	return count;
}

static ssize_t min_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, MINUTE_REG));
}

static ssize_t min_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int min;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &min);
	if(UPPER_NIBBLE(min) > 5 || LOWER_NIBBLE(min) > 9)
		return -EINVAL;

	write_reg(client, MINUTE_REG, (char)min);
	return count;
}

static ssize_t hour_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, HOUR_REG));
}

static ssize_t hour_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int hour;
	char upper, lower;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &hour);
	upper = UPPER_NIBBLE(hour);
	lower = LOWER_NIBBLE(hour);
	if(upper > 2 || (upper == 2 && lower > 4) || lower > 9)
		return -EINVAL;

	write_reg(client, HOUR_REG, (char)hour);
	return count;
}

static ssize_t week_day_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	char day;
	char day_name[16];
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	day = read_reg(client, DAY_REG);
	switch(day)
	{
		case 1:
			strcpy(day_name, "Monday");
			break;
		case 2:
			strcpy(day_name, "Tuseday");
			break;
		case 3:
			strcpy(day_name, "Wednesday");
			break;
		case 4:
			strcpy(day_name, "Thursday");
			break;
		case 5:
			strcpy(day_name, "Friday");
			break;
		case 6:
			strcpy(day_name, "Saturday");
			break;
		case 7:
			strcpy(day_name, "Sunday");
			break;
		default:
			strcpy(day_name, "Unkown");
	}
	return sprintf(buf, "%s", day_name);
}

static ssize_t week_day_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	char day_buf[count];
	int i, day;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	if(buf[count - 1] == '\n') {
		strncpy(day_buf, buf, count - 1);
		day_buf[count - 1] = '\0';
	}
	else
		strcpy(day_buf, buf);

	/*to lower case */
	for(i = 0; i < count; i++)
		if(day_buf[i] >= 65 && day_buf[i] <= 90)day_buf[i] += 32;

	if(strcmp(day_buf, "monday") == 0)
		day = 1;
	else if(strcmp(day_buf, "tuseday") == 0)
		day = 2;
	else if(strcmp(day_buf, "wednesday") == 0)
		day = 3;
	else if(strcmp(day_buf, "thursday") == 0)
		day = 4;
	else if(strcmp(day_buf, "friday") == 0)
		day = 5;
	else if(strcmp(day_buf, "saturday") == 0)
		day = 6;
	else if(strcmp(day_buf, "sunday") == 0)
		day = 7;
	else
		return -EINVAL;

	write_reg(client, DAY_REG, (char)day);
	return count;
}

static ssize_t month_day_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, DATE_REG));
}

static ssize_t month_day_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int date;
	char upper, lower;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &date);
	upper = UPPER_NIBBLE(date);
	lower = LOWER_NIBBLE(date);
	if(upper > 3 || (upper == 3 && lower > 1) || lower > 9 || date == 0)
		return -EINVAL;

	write_reg(client, DATE_REG, (char)date);
	return count;
}

static ssize_t month_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	char month;
	char month_name[16];
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	month = read_reg(client, MONTH_REG);
	switch(month)
	{
		case 0x01:
			strcpy(month_name, "January");
			break;
		case 0x02:
			strcpy(month_name, "February");
			break;
		case 0x03:
			strcpy(month_name, "March");
			break;
		case 0x04:
			strcpy(month_name, "April");
			break;
		case 0x05:
			strcpy(month_name, "May");
			break;
		case 0x06:
			strcpy(month_name, "June");
			break;
		case 0x07:
			strcpy(month_name, "July");
			break;
		case 0x08:
			strcpy(month_name, "August");
			break;
		case 0x09:
			strcpy(month_name, "September");
			break;
		case 0x10:
			strcpy(month_name, "October");
			break;
		case 0x11:
			strcpy(month_name, "November");
			break;
		case 0x12:
			strcpy(month_name, "December");
			break;
		default:
			strcpy(month_name, "Unkown");
	}
	return sprintf(buf, "%s", month_name);
}

static ssize_t month_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	char month_buf[count];
	int i, month;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	if(buf[count - 1] == '\n') {
		strncpy(month_buf, buf, count - 1);
		month_buf[count - 1] = '\0';
	}
	else
		strcpy(month_buf, buf);

	/*to lower case */
	for(i = 0; i < count; i++)
		if(month_buf[i] >= 65 && month_buf[i] <= 90)month_buf[i] += 32;

	if(strcmp(month_buf, "januart") == 0)
		month = 0x01;
	else if(strcmp(month_buf, "february") == 0)
		month = 0x02;
	else if(strcmp(month_buf, "march") == 0)
		month = 0x03;
	else if(strcmp(month_buf, "april") == 0)
		month = 0x04;
	else if(strcmp(month_buf, "may") == 0)
		month = 0x05;
	else if(strcmp(month_buf, "june") == 0)
		month = 0x06;
	else if(strcmp(month_buf, "july") == 0)
		month = 0x07;
	else if(strcmp(month_buf, "august") == 0)
		month = 0x08;
	else if(strcmp(month_buf, "september") == 0)
		month = 0x09;
	else if(strcmp(month_buf, "october") == 0)
		month = 0x10;
	else if(strcmp(month_buf, "november") == 0)
		month = 0x11;
	else if(strcmp(month_buf, "december") == 0)
		month = 0x12;
	else
		return -EINVAL;

	write_reg(client, MONTH_REG, (char)month);
	return count;
}

static ssize_t year_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, YEAR_REG));
}

static ssize_t year_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int year;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &year);
	if(UPPER_NIBBLE(year) > 9 || LOWER_NIBBLE(year) > 9)
		return -EINVAL;

	write_reg(client, YEAR_REG, (char)year);
	return count;
}


static DEVICE_ATTR(second, 0644, sec_show, sec_store);
static DEVICE_ATTR(minute, 0644, min_show, min_store);
static DEVICE_ATTR(hour, 0644, hour_show, hour_store);
static DEVICE_ATTR(week_day, 0644, week_day_show, week_day_store);
static DEVICE_ATTR(month_day, 0644, month_day_show, month_day_store);
static DEVICE_ATTR(month, 0644, month_show, month_store);
static DEVICE_ATTR(year, 0644, year_show, year_store);

static int ds3231_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	if(device_create_file(&client->dev, &dev_attr_second) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_second;
	}

	if(device_create_file(&client->dev, &dev_attr_minute) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_minute;
	}

	if(device_create_file(&client->dev, &dev_attr_hour) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_hour;
	}

	if(device_create_file(&client->dev, &dev_attr_week_day) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_week_day;
	}

	if(device_create_file(&client->dev, &dev_attr_month_day) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_month_day;
	}

	if(device_create_file(&client->dev, &dev_attr_month) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_month;
	}

	if(device_create_file(&client->dev, &dev_attr_year) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_year;
	}

	return 0;

err_year:
	device_remove_file(&client->dev, &dev_attr_month);
err_month:
	device_remove_file(&client->dev, &dev_attr_month_day);
err_month_day:
	device_remove_file(&client->dev, &dev_attr_week_day);
err_week_day:
	device_remove_file(&client->dev, &dev_attr_hour);
err_hour:
	device_remove_file(&client->dev, &dev_attr_minute);
err_minute:
	device_remove_file(&client->dev, &dev_attr_second);
err_second:
	return -1;
}

static int ds3231_remove(struct i2c_client *client)
{
	device_remove_file(&client->dev, &dev_attr_year);
	device_remove_file(&client->dev, &dev_attr_month);
	device_remove_file(&client->dev, &dev_attr_month_day);
	device_remove_file(&client->dev, &dev_attr_week_day);
	device_remove_file(&client->dev, &dev_attr_hour);
	device_remove_file(&client->dev, &dev_attr_minute);
	device_remove_file(&client->dev, &dev_attr_second);
	return 0;
}

static const struct i2c_device_id ds3231_id[] = {
	{"ds3231sample", 0},
	{},
};

static const struct of_device_id ds3231_match[] = {
	{ .compatible = "unknown,ds3231sample", },
	{ },
};

MODULE_DEVICE_TABLE(i2c, ds3231_id);

static struct i2c_driver ds3231_i2c_driver = {
	.driver = {
		.owner = THIS_MODULE,
		.name = "ds3231sample",
		.of_match_table = of_match_ptr(ds3231_match),
	},
	.probe = ds3231_probe,
	.remove = ds3231_remove,
	.id_table = ds3231_id,
};

module_i2c_driver(ds3231_i2c_driver);

MODULE_LICENSE("GPL");

Choć kod wydaje się długawy to duża jego część powinna być dla Ciebie zrozumiała.

Zacznijmy od tego co znasz na pewno czyli funkcji probe oraz remove. W funkcji probe są tworzone atrybuty naszego urządzenia, a w funkcji remove są one usuwane. Na atrybuty w tym przypadku składają się poszczególne miary czasu tj. godzina, dzień, miesiąc, rok itd., które odpowiadają poszczególnym rejestrom urządzenia.

Funkcje show i store również powinny być tobie znane, jedyną nowością w nich jest zapis i odczyt danych po magistrali I2C.

Aby zapis i odczyt danych był możliwie prosty opakowaliśmy standardową funkcję i2c_transfer w funkcje write_reg oraz read_reg. Funkcje te wypełniają odpowiednio strukturę i2c_msg tak aby można było odczytać lub zapisać dane przy pomocy funkcji i2c_transfer.

Kolejną nowością, ale tylko pozorną, są struktura i2c_driver oraz makro module_i2c_driver. Nie jest to nic więcej jak odpowiedniki struktury platform_driver oraz makra module_platform_driver z tą różnicą, że są one dedykowane urządzeniom komunikującym się po I2C. Makro module_i2c_driver przygotowuje standardową funkcję init i exit, które oprócz tego, że rejestrują sterownik platformowy to rejestruje również urządzenie I2C.

Tak więc wydawało się, że jest sporo kodu, a tak naprawdę poznaliśmy wcale nie tak dużo nowych rzeczy.

Makefile wygląda następująco:

obj-m += 13_i2c.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

Przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Device-tree

Wpis dla BBB oraz RPi będą do siebie łudząco podobne, będą się jedynie różnić magistralą do której są podłączone.

Nasz wpis będzie zawierał dwie właściwości- compatible oraz reg. Zapewne już wiesz czym jest compatible, a reg to adres urządzenia na magistrali I2C.

BeagleBone Black

W przypadku BBB w pliku arch/arm/boot/dts/am335x-boneblack-uboot-univ.dts dodaj następujący wpis:

&i2c2 {
     ds3231sample: ds3231sample@68 {
         compatible = "unknown,ds3231sample";
         reg = <0x68>;
     };
};

Device-tree przebuduj za pomocą komendy make tak jak jest to opisane w lekcji 10 i wgraj je na swoją płytkę.

Raspberry Pi 4

W przypadku RPi w pliku arch/arm/boot/dts/bcm2711-rpi-4-b.dts dodaj następujący wpis:

&i2c1 {
     ds3231sample: ds3231sample@68 {
         compatible = "unknown,ds3231sample";
         reg = <0x68>;
     };
};

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp. Załaduj moduł:

sudo insmod 13_i2c.ko

I przejdź do katalogu urządzenia:

# BBB
cd /sys/bus/i2c/devices/2-0068
# RPi
cd /sys/bus/i2c/devices/1-0068

Powinieneś zobaczyć kilka plików w tym atrybuty, które utworzyłeś w module. Każdy atrybut możesz odczytać oraz zapisać:

cat second
echo 10 > second

Inne podsystemy

W kernelu będą dostępne również analogiczne interfejsy do innych magistrali jak np. SPI, ale są również dostępne interfejsy nieco wyższego poziomu jak np. RTC, który to udostępnia interfejs do zegarów bez względu na używany interfejs komunikacyjny.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 12- Synchronizacja

Wstęp

W tej lekcji zapoznamy się z metodami synchronizacji. Ponieważ zajęliście się linuksowym kernelem to zakładam, że całkiem dobrze programujecie i wiecie czym jest mutex oraz spinlock i ogólnie na czym polega synchronizacja.

Implementacja

W tej lekcji zaimplementujemy aż 5 modułów, ale bardzo prostych modułów. Dwa jako przykład dla muteksa oraz dwa jako przykład dla spinlocka oraz jeden, który eksportuje potrzebne symbole. W obu przypadkach zasada działania będzie taka sama, moduł będzie blokować muteksa lub spinlocka w funkcji init co uniemożliwi załadowanie się drugiemu modułowi. Mutex lub spinlock jest odblokowywany w funkcji exit.

Eksportowanie symboli

Aby umożliwić dowolną kolejność ładowania modułów musimy wyeksportować muteksa oraz spinlocka w innym module, jego kod jest bardzo prosty i wygląda tak:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/spinlock.h>

DEFINE_MUTEX(my_mutex);
DEFINE_SPINLOCK(my_spinlock);

EXPORT_SYMBOL(my_mutex);
EXPORT_SYMBOL(my_spinlock);

static int __init exporter_init(void) {
	return 0;
}
static void __exit exporter_exit(void) {
}

module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");

Jak widać kod jest bardzo prosty, jeśli zapoznałeś się z poprzednią lekcją to w zasadzie nie wymaga on większego komentarza. Jedyną nowością tutaj są dwa makra- DEFINE_MUTEX oraz DEFINE_SPINLOCK. Są to makra służące do inicjalizacji odpowiednio muteksa i spinlocka.

Mutex

Najpierw się zajmiemy muteksem. Jak zostało powiedziane wcześniej, zostaną utworzone dwa moduły, które będą blokować wzajemnie swoje załadowanie. Kod pierwszego modułu wygląda następująco:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mutex.h>

extern struct mutex my_mutex;

static int __init mutex_1_init(void)
{
	mutex_lock(&my_mutex);
	pr_info("MUTEX 1 init\n");
	return 0;
}
static void __exit mutex_1_exit(void)
{
	mutex_unlock(&my_mutex);
	pr_info("MUTEX 1 exit\n");
}

module_init(mutex_1_init);
module_exit(mutex_1_exit);
MODULE_LICENSE("GPL");

Drugi kod jest niemalże identyczny jak pierwszy i wygląda tak:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mutex.h>

extern struct mutex my_mutex;

static int __init mutex_2_init(void)
{
	mutex_lock(&my_mutex);
	pr_info("MUTEX 2 init\n");
	return 0;
}
static void __exit mutex_2_exit(void)
{
	mutex_unlock(&my_mutex);
	pr_info("MUTEX 2 init\n");
}

module_init(mutex_2_init);
module_exit(mutex_2_exit);
MODULE_LICENSE("GPL");

W kodzie pojawiają się dwie nowe funkcje- mutex_lock oraz mutex_unlock. Służą one odpowiednio do zablokowania muteksa i jego zwolnienia. Obie przyjmują jeden parametr i jest nim wskaźnik do muteksa.

Spinlock

Spinlock- czy jak kto woli rygiel pętlowy(tak, takie jest polskie tłumaczenie). Implementacja będzie bliźniacza do przykładu z muteksem:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>

extern struct spinlock my_spinlock;

static int __init spinlock_1_init(void)
{
	spin_lock(&my_spinlock);
	pr_info("SPINLOCK 1 init\n");
	return 0;
}
static void __exit spinlock_1_exit(void)
{
	spin_unlock(&my_spinlock);
	pr_info("SPINLOCK 1 exit\n");
}

module_init(spinlock_1_init);
module_exit(spinlock_1_exit);
MODULE_LICENSE("GPL");

Kod drugiego modułu jest analogiczny:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>

extern struct spinlock my_spinlock;

static int __init spinlock_2_init(void)
{
	spin_lock(&my_spinlock);
	pr_info("SPINLOCK 2 init\n");
	return 0;
}
static void __exit spinlock_2_exit(void)
{
	spin_unlock(&my_spinlock);
	pr_info("SPINLOCK 2 exit\n");
}

module_init(spinlock_2_init);
module_exit(spinlock_2_exit);
MODULE_LICENSE("GPL");

W tym przypadku ponownie pojawiły się dwie nowe funkcje- spin_lock oraz spin_unlock, które to służą odpowiednio do zablokowania i odblokowania spinlocka. Obie przyjmują jeden parametr- wskaźnik do spinlocka.

Mutex a spinlock

Patrząc na kod użycie obu mechanizmów synchronizacji wygląda identycznie i może się pojawić pytanie- po co mnożyć byty ponad potrzebę? Skoro użycie wygląda właściwie identycznie i oba mechanizmy robią to samo to może się wydawać bez sensu.

Rzeczywiście oba mechanizmy robią to w zasadzie to samo- czyli blokują dostęp do jakiś sekcji kodu, ale robią to w nieco inny sposób. Mutex może być zastosowany w sytuacji kiedy dane zadanie może zostać uśpione, spinlock może zostać użyty w zasadzie każdej sytuacji(choć nie zawsze będzie to wskazane) ponieważ jego implementacja uniemożliwia uśpienie danego zadania, jest to tzw. aktywna synchronizacja. Spinlocków będziemy używać np. wewnątrz przerwań ponieważ przerwania nie mogą zostać uśpione.

Kompilacja

No to jeszcze szybki makefile:

obj-m += 12_symbols_exporter.o 12_mutex_1.o 12_mutex_2.o 12_spinlock_1.o 12_spinlock_2.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

Przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp.

Testowanie muteksa i spinlocka zacznimy od załadowania modułu eksportującego symbole:

sudo insmod 12_symbols_exporter.ko

W dalszej części najlepiej będzie jeśli otworzysz dwie sesje SSH, wtedy będzie lepiej widać co się dzieje.

Test zacznijmy od muteksa:

# pierwsza sesja ssh
sudo insmod 12_mutex_1.ko
# druga sesja ssh
sudo insmod 12_mutex_2.ko

Pierwszy moduł powinien się załadować, a drugi powinien wisieć. Możesz teraz odładować pierwszy moduł:

# pierwsza sesja ssh
sudo rmmod 12_mutex_1.ko

W momencie jego odładowania drugi moduł został załadowany. Teraz możesz ponownie załadować moduł 12_mutex_1.ko co objawi się ponownie tym, że komenda insmod będzie wisieć.

Teraz przetestujmy spinlocka w analogiczny sposób:

# pierwsza sesja ssh
sudo insmod 12_spinlock_1.ko
# druga sesja ssh
sudo insmod 12_spinlock_2.ko

Oczywiście ładowanie modułu 12_spinlock_2.ko wisi, ale co więcej, w pierwszej sesji również nie jesteś raczej w stanie nic zrobić. Wynika to właśnie z faktu, że spinlock zajął znaczącą część czasu pracy procesora co uniemożliwia pracę z urządzeniem. Po zresetowaniu twojego BBB lub RPi wszystko wróci do normy.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 11- Eksportowanie symboli

Wstęp

W tej krótkiej lekcji dowiemy się jak używać symboli zdefiniowanych w jednym module w innym module. Co rozumiem przez symbol? Przez symbol rozumiem zmienną lub funkcję. Umożliwienie użycia symbolu innym modułom nazywamy najczęściej po prostu eksportowaniem symboli.

Implementacja

Jak zwykle użyjemy naszej ulubionej diody LED do testu. W tym przypadku napiszemy dwa moduły- jeden, który będzie eksportować numer pinu do którego jest podłączona dioda LED. Drugi moduł będzie odczytywał ten numer i będzie dokonywał wszystkich niezbędnych ustawień pinu GPIO.

Zacznijmy od modułu, który eksportuje symbol, jego kod wygląda następująco:

#include <linux/init.h>
#include <linux/module.h>

int led_pin = 48;

EXPORT_SYMBOL(led_pin);

static int __init led_init(void)
{
	return 0;
}
static void __exit led_exit(void)
{
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

Jak widać kod jest niezwykle prosty, jest niemalże tak samo prosty jak nasz pierwszy moduł. Jedyną nowością w tym kodzie jest makro EXPORT_SYMBOL, które to umożliwia użycie danego symbolu innym modułom. Przyjmuje ono jeden parametr- symbol, który ma zostać wyeksportowany.

Teraz zajmijmy się drugim modułem, który również jest prosty:

#include <linux/gpio.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

extern int led_pin;

static int __init led_init(void)
{
	if (!gpio_is_valid(led_pin)){
      pr_err("Invalid GPIO pin!!1\n");
      return -ENODEV;
   }
	gpio_request(led_pin, "gpioLED");
	gpio_direction_output(led_pin, 1);
	gpio_set_value(led_pin, 1);
	gpio_export(led_pin, false);	
	return 0;
}
static void __exit led_exit(void)
{
	gpio_set_value(led_pin, 0);
	gpio_unexport(led_pin);
	gpio_free(led_pin);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

Jeśli przerobiłeś poprzednie lekcje to również powinieneś rozumieć ten kod. Jedyną nowością tutaj jest linijka:

extern int led_pin;

Słowo kluczowe extern oznacza, że dany symbol został zdefiniowany poza modułem. Po słowie kluczowym extern umieszczamy deklaracje zmiennej lub funkcji.

Tym razem budujemy dwa pliki za pomocą Makefile’a:

obj-m += 11_symbol_exporter.o 11_symbol_importer.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

Przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp.

Testowanie tego modułu jest bardzo proste:

sudo insmod 11_symbol_exporter.ko
sudo insmod 11_symbol_importer.ko

Po wykonaniu tych komend dioda powinna się zapalić.

Odwrotna sekwencja ładowania modułów nie powiedzie się ponieważ nie może zostać załadowany moduł, który importuje symbol, który nie został wyeksportowany.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz

Lekcja 10- Device-tree

Wstęp

W tej lekcji zapoznamy się ze sposobem użycia device-tree z poziomu modułu kernelowego. Jeśli nie masz najmniejszego pojęcia czym jest device-tree to odsyłam do lekcji 6 z kursu budowania Linuksa.

Device-tree

Zanim użyjemy device-tree w sterowniku to najpierw musimy dodać odpowiednie wpisy w nim, które będą używane przez nasz moduł. W tej lekcji napiszemy sterownik do obsługi diody LED(no bo do czego by innego można by napisać sterownik), pin do którego jest podłączona dioda będzie zdefiniowany w device-tree. Poniżej jest zademonstrowane jak dodać odpowiedni wpis zarówno dla BBB jak i RPi. Kod modułu będzie identyczny dla obu platform. To, że kod będzie identyczny dla obu platform jest w tym przypadku zasługą device-tree.

Nasz wpis w device-tree będzie się składał z trzech własności- z comptible’a(powinieneś wiedzieć co to pole oznacza), z pinu oraz z początkowego stanu diody(włączona lub wyłączona).

BeagleBone Black

Aby zmodyfikować device-tree przejdź do katalogu z kernelem dla BBB. Otwórz plik arch/arm/boot/dts/am335x-boneblack-uboot-univ.dts. Jest to plik na podstawie, którego jest generowane docelowe device-tree dla BBB. Dodaj dwa wpisy pod węzłem root(czyli węzeł /). My dodamy dwie diody LED, kod po naszej modyfikacji powinien wyglądać następująco:

/ {
     model = "TI AM335x BeagleBone Black";
     compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
 
     chosen {
         base_dtb = "am335x-boneblack-uboot-univ.dts";                                                                                                                                                                                     
         base_dtb_timestamp = __TIMESTAMP__;
     };
 
     myled0 {
          compatible = "my_led";
          pin = <48>;
          state = <0>;
     };
     myled1 {
          compatible = "my_led";
          pin = <49>;
          state = <1>;
     };
 };

Zapisz zmiany i przebuduj kernel za pomocą komendy:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

Po kompilacji podmień plik /boot/dtb/am335x-boneblack-uboot-univ.dtb na karcie pamięci nowym zmodyfikowanym device-tree, które znajduje się pod ścieżką arch/arm/boot/dts/am335x-boneblack-uboot-univ.dts.

Diody podłącz oczywiście do pinów, które zdefiniowałeś w device-tree i jak zwykle pamiętaj o użyciu rezystorów podczas podłączania.

Raspberry Pi 4

Aby zmodyfikować device-tree przejdź do katalogu z kernelem dla RPi. Otwórz plik arch/arm/boot/dts/bcm2711-rpi-4-b.dts. Wprawne oko zauważyło już pewnie, że w ścieżce odnosimy się do architektury ARM, a nie ARM64. Nie jest to przypadek. Dla RPi devicetree dla architektury ARM i ARM64 są identyczne. Plik arch/arm64/boot/dts/bcm2711-rpi-4-b.dts zawiera tylko jedną linijkę- import pliku arch/arm/boot/dts/bcm2711-rpi-4-b.dts.

W podanym pliku na samym jego końcu dodaj dwa wpisy pod węzłem root(czyli węzeł /):

/ {
	__overrides__ {
		act_led_gpio = <&act_led>,"gpios:4";
		act_led_activelow = <&act_led>,"gpios:8";
		act_led_trigger = <&act_led>,"linux,default-trigger";

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

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

		sd_poll_once = <&emmc2>, "non-removable?";
		spi_dma4 = <&spi0>, "dmas:0=", <&dma40>,
			   <&spi0>, "dmas:8=", <&dma40>;
	};

	myled0 {
		compatible = "my_led";
		pin = <23>;
		state = <0>;
	};
	myled1 {
		compatible = "my_led";
		pin = <24>;
		state = <1>;
	};
};

Zapisz zmiany i przebuduj kernel za pomocą komendy:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Po kompilacji podmień plik bcm2711-rpi-4-b.dtb na partycji boot na karcie pamięci używanej przez RPi nowym zmodyfikowanym device-tree, które znajduje się pod ścieżką arch/arm64/boot/dts/ bcm2711-rpi-4-b.dtb.

Diody podłącz oczywiście do pinów, które zdefiniowałeś w device-tree i jak zwykle pamiętaj o użyciu rezystorów podczas podłączania.

Implementacja

Podobnie jak w poprzedniej lekcji implementowany moduł będzie sterownikiem platformowym. Kod wygląda następująco:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/of.h>

#define MOD_NAME "myled"

struct led_data {
	u32 pin;
	u32 state;
};

static ssize_t led_state_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	struct led_data *data = platform_get_drvdata(pdev);
	int state = gpio_get_value(data->pin);
	return sprintf(buf, "%i", state);
}

static ssize_t led_state_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	struct platform_device *pdev = container_of(dev, struct platform_device, dev);
	struct led_data *data = platform_get_drvdata(pdev);
	int state;
	if(sscanf(buf, "%i", &state) < 1) {
		return -EINVAL;
	}

	if(state < 0) {
		return -EINVAL;
	}

	gpio_set_value(data->pin, state);
	return count;
}

static DEVICE_ATTR(led_state, 0644, led_state_show, led_state_store);

static int my_led_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	struct led_data *data;

	data = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);
	if(data == NULL) {
		pr_err("%s: cannot allocate memory!!!\n", MOD_NAME);
		return -ENOMEM;
	}

	if(device_create_file(&pdev->dev, &dev_attr_led_state) != 0)	{
		pr_err("%s: cannot create sysfs entry!!!\n", MOD_NAME);
		goto err1;
	}

	of_property_read_u32(np, "pin", &data->pin);
	of_property_read_u32(np, "state", &data->state);
	if (!gpio_is_valid(data->pin)){
		pr_err("Invalid GPIO pin!!!\n");
		goto err2;
	}

	gpio_request(data->pin, "gpioLED");
	gpio_direction_output(data->pin, data->state);
	gpio_export(data->pin, false);

	platform_set_drvdata(pdev, data);
	pr_info("my_led pin: %i\n", data->pin);
	return 0;

err2:
	device_remove_file(&pdev->dev, &dev_attr_led_state);
err1:
	return -1;
}

static int my_led_remove(struct platform_device *pdev)
{
	struct led_data *data = platform_get_drvdata(pdev);
	gpio_set_value(data->pin, 0);
	gpio_unexport(data->pin);
	gpio_free(data->pin);
	device_remove_file(&pdev->dev, &dev_attr_led_state);
	return 0;
}

static const struct of_device_id myled_match[] = {
	{ .compatible = "my_led", },
	{ },
};
MODULE_DEVICE_TABLE(of, myled_match);

static struct platform_driver mypdrv = {
	.probe = my_led_probe,
	.remove = my_led_remove,
	.driver = {
		.name     = "my_led",
		.of_match_table = of_match_ptr(myled_match),
	},
};

module_platform_driver(mypdrv);

MODULE_LICENSE("GPL");

Jak widać kod nie różni się jakoś bardzo od kodu z lekcji 9. Zupełnie nowym elementem, który dochodzi w tym kodzie jest struktura myled_match o typie of_device_id:

static const struct of_device_id myled_match[] = {
	{ .compatible = "my_led", },
	{ },
};

Struktura ta zawiera listę urządzeń, która ma być obsługiwana przez dany sterownik. Struktura ta jest przypisywana do pola of_match_table w strukturze driver wewnątrz struktury platform_driver.

Funkcja probe różni się nieznacznie od jej odpowiednika z poprzedniej lekcji. Po pierwsze, na samym początku alokujemy pamięć dla struktury data typu led_data(struktura ta jest zdefiniowana na samym początku kodu):

data = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);

Z poprzednich lekcji zapewne pamiętasz, że do alokacji pamięci w kernelu mamy takie funkcje jak kmalloc i kzalloc(ta funkcja oprócz alokacji pamięci czyści zaalokowany obszar pamięci). Funkcja devm_kzalloc też oczywiście alokuje pamięć, ale dodatkowo przypisuje ten obszar pamięci do danego urządzenia. Takie podejście powoduje, że nie musimy ręcznie zwalniać tak zaalokowanej pamięci, zostanie ona zwolniona w momencie usunięcia tego urządzenia z systemu.

Kolejną różnicą są wywołania funkcji of_property_read_u32:

of_property_read_u32(np, "pin", &data->pin);
of_property_read_u32(np, "state", &data->state);

Funkcje te odczytują dane właściwości z węzła w device-tree(oczywiście dla każdego typu danych występuje analogiczna funkcja). W tym przypadku odczytane wartości lądują od razu w strukturze data. Odczytane dane są następnie użyte do skonfigurowania pinów GPIO.

Na końcu funkcji przypisujemy strukturę data jako dane dla tego urządzenia platformowego za pomocą funkcji platform_set_drvdata. Dzięki temu będziemy mogli sprawdzać do jakiego pinu jest podłączona dioda w innych funkcjach.

Funkcja remove też się nieco różni od swojego odpowiednika z poprzedniej lekcji, ale w tym przypadku właściwie jedyną różnicą jest to, że najpierw odczytujemy dane urządzenia za pomocą funkcji platform_get_drvdata i na podstawie odczytanych danych zwracamy pin GPIO do systemu.

W funkcjach show i store również używamy funkcji platform_get_drvdata do odczytu danych urządzenia. Tylko w tym przypadku musimy nieco pokombinować ponieważ do funkcji show oraz store nie jest przekazywany bezpośrednio struktura platform_device tylko jej pole- struktura device. Aby otrzymać interesującą strukturę platform_device używamy makra container_of, które to zwraca nam strukturę, w której znajduje się nasza zmienna, w tym przypadku jest to struktura device.

I to chyba tyle, mam nadzieje, że wszystko jest jasne, szczególnie jeśli chodzi o ostatni akapit i strukturę container_of, gdyż może się to wydawać trochę zagmatwane na początku.

Przygotuj Makefile:

obj-m += 10_devicetree.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

I przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp.

Testowanie tego modułu będzie zbliżone do testu z poprzedniej lekcji. Załaduj moduł do systemu:

sudo insmod 10_devicetree.ko

Jeśli poprawnie wprowadziłeś zmiany do devicetree oraz moduł nie posiada żadnych błędów to w sysfs powinny powstać odpowiednie pliki:

# Pierwsza dioda
cd /sys/devices/platform/myled0
# Druga dioda
cd /sys/devices/platform/myled1

W obu katalogach powinien być plik led_state. Zapisuj do tych plików 0 lub 1 aby zmieniać stany diod.

Dodatkowo, jeśli wykonasz komendę ls w tych katalogach zobaczysz, że znajduje się tam katalog of_node, katalog ten jest reprezentacją wpisu w devicetree w sysfs. Wykonaj komendę ls na tym katalogu:

ls of_node
compatible  name  pin  state

Jak wszystkie właściwości wpisu myled w devicetree są odwzorowane w postaci plików.

Dla formalności, katalog of_node jest dowiązaniem symbolicznym do /sys/firmware/devicetree/base/myledX gdzie X to 0 lub 1. Jest tak ponieważ w katalogu /sys/firmware/devicetree/base znajduje się odwzorowanie całego devicetree.

Zaszufladkowano do kategorii Kurs pisania sterowników | Dodaj komentarz