Samstag, 27. Dezember 2025

PWM-Signal auf dem Raspberry PI PINs läst sich nicht ansteuern

Beim Prüfen der PWM-Ausgänge musste ich feststellen, dass sich die Pins nicht ansprechen ließen.

Hierfür habe ich ein C#-Projekt mit .NET erstellt, das auf die GPIOs zugreift und ein PWM-Signal zur Steuerung von zwei Servos einrichtet.
Selbst mit direkten Shell-Kommandos ließen sich die PWM Pins auf dem Raspberry Pi nicht einstellen.


Verwendete Hardware

  • Raspberry Pi 4
  • Raspbian OS
  • Mini-Servo

Ziel

Einen GPIO-Pin für PWM-Signale korrekt einrichten, sodass ein Servo zuverlässig gesteuert werden kann.

  • GPIO12 auf PWM festlegen
  • GPIO13 auf PWM festlegen


Welche Version von Raspbian OS ist installiert?

Mit folgendem Befehl werden mehrere Zeilen ausgegeben, die Informationen über die aktuell verwendete Linux-Version liefern:

 hostnamectl  




PWM zunächst einrichten

Über die config.txt kann der PWM-Eintrag gesetzt werden.
In älteren Raspbian-Versionen befand sich diese Datei unter /boot/config, der Pfad hat sich jedoch geändert zu:

 /boot/firmware/config.txt  

Bearbeiten der Datei:

 sudo nano /boot/firmware/config.txt  

Ganz nach unten scrollen und folgenden Eintrag setzen:

 dtoverlay=pwm-2chan  

HINWEIS: Dieser Eintrag ist Teil des Problems. Die korrekte Lösung wird im letzten Abschnitt unter „Dauerhaft einstellen“ beschrieben.

Speichern mit STRG + O, bestätigen mit ENTER.
Nano mit STRG + X verlassen.

Anschließend muss das Betriebssystem neu gestartet werden:

 sudo reboot  

Servo anschließen

Der Servo kann nun am Raspberry Pi angeschlossen werden.
Die Stromversorgung sollte jedoch nicht über die GPIO-Pins des Raspberry Pi erfolgen. Eine Ausnahme gilt nur für Mini-Servos ohne nennenswerte Last und mit sehr geringem Stromverbrauch.

Zwar kann die USB-C-Stromversorgung bis zu 3 A liefern, dennoch empfiehlt es sich, unter 500 mA zu bleiben und die Stromaufnahme nicht auszureizen.


Einrichtung über die Shell

Zunächst ein einfacher Test, um einen Servo direkt über die Shell anzusteuern.

Prüfen, ob der PWM-Chip verfügbar ist

 ls /sys/class/pwm  

Das erwartete Ergebnis ist pwmchip0.
Andernfalls muss der Chip über die config.txt korrekt eingerichtet werden.




Prüfen, ob pwm0 vorhanden ist

 ls /sys/class/pwm/pwmchip0  

In meinem Fall wurde nur pwm1 angezeigt.




PWM-Kanal exportieren

 echo 0 | sudo tee /sys/class/pwm/pwmchip0/export  

Danach sollte auch pwm0 verfügbar sein.




PWM für Servo einstellen

Periode: 20 ms

 echo 20000000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/period  

Pulsweite für Mittelstellung (1,5 ms)

 echo 1500000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/duty_cycle  

PWM aktivieren

 echo 1 | sudo tee /sys/class/pwm/pwmchip0/pwm0/enable  

An dieser Stelle sollte der Servo in die Mittelstellung fahren.

Servo nach links bewegen (Pulsweite 1 ms)

 echo 1000000 | sudo tee /sys/class/pwm/pwmchip0/pwm0/duty_cycle  

Servo bewegt sich weiterhin nicht

Wenn sich der Servo trotz dieser Einstellungen nicht bewegt, liegt die Ursache sehr wahrscheinlich an der Pin-Konfiguration.
Als Nächstes wird der Status des GPIO-Pins geprüft:

 raspi-gpio get 12  

In meinem Fall erschien die Meldung „command not found“.




Alternative Prüfung

 pinctrl get 12  

Damit lässt sich ebenfalls die aktuelle Konfiguration von GPIO12 auslesen.
In meinem Fall ergab sich folgendes Ergebnis:



raspi-gpio-Paket installieren?

In aktuellen Versionen von Raspbian OS ist das Paket raspi-gpio nicht mehr standardmäßig installiert.
Ein Nachinstallieren ist grundsätzlich möglich:

 sudo apt update  
 sudo apt install raspi-gpio  

In meinem Fall trat dabei jedoch ein Fehler auf.


Offenbar ist hier entweder eine Anpassung seitens der Distribution erforderlich – oder das Paket wird nicht mehr vorgesehen.


Pin umstellen (Pin-Multiplexing)

In meinem Fall war GPIO12 als INPUT konfiguriert und musste über Pin-Multiplexing umgestellt werden.

GPIO12 auf PWM0 setzen:

 sudo pinctrl set 12 a0  

Die Änderung sollte ohne Neustart wirksam sein.
Bei mir bewegte sich der Servo danach endlich wie erwartet.


Dauerhaft einstellen

Rückblickend hätte ich diese Konfiguration direkt zu Beginn vollständig setzen sollen.
Hätte ich das früher gewusst, hätte ich mir viele Zwischenschritte erspart, da sich die meisten Lösungsansätze nur auf kurzfristige Workarounds beziehen.

Für eine dauerhafte PWM-Konfiguration muss folgender Eintrag in der Datei
/boot/firmware/config.txt gesetzt werden:

 dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4  

Danach speichern mit STRG + O, bestätigen mit ENTER, Nano mit STRG + X beenden und anschließend neu starten:

 sudo reboot  

Fazit

Offensichtlich wurde mit der neuen Betriebssystemversion deutlich aufgeräumt. Viele Funktionen scheinen heute durch unterschiedliche Bibliotheken mehrfach vorhanden zu sein oder wurden neu strukturiert.

Erst mit Unterstützung von Microsoft Copilot und ChatGPT bin ich – nach zahlreichen Fragen und Fehlversuchen – auf diese Lösung gestoßen.
Über die klassische Google-Suche bzw. Gemini führte der Weg ebenfalls nicht direkt zum Ziel.

Ein möglicher Grund dafür ist der Wechsel des Raspbian OS von Bookworm auf Trixie, der intern offenbar einige Änderungen an der GPIO- und PWM-Konfiguration mit sich gebracht hat.

Freitag, 26. Dezember 2025

Codexzier DirectSight - Entwicklung (Teil 1)

Dienst mit .NET für Raspberry Pi entwickeln

Was muss ich tun, um einen Raspberry Pi mit Kamera und zwei Servos über einen Windows-PC anzusteuern?

An dieser Stelle ist klar: Dieses Thema lässt sich nicht in einem kurzen Blogpost mit zwei oder drei Seiten vollständig abhandeln. In Zeiten von Künstlicher Intelligenz und sogenanntem Vibe Coding sind größere Vorhaben jedoch deutlich schneller umsetzbar als noch vor fünf Jahren.
Trotzdem habe ich festgestellt, dass selbst der Einstieg – obwohl er nur ein Teilschritt des Gesamtprojekts ist – überraschend zeitintensiv war. Insgesamt habe ich mehr als drei Stunden an einem Tag investiert. Mit dem entsprechenden Vorwissen hätte ich das vermutlich in 30 Minuten erledigt.

Genau darum geht es mir jedoch: Lernen mit Unterstützung von KI, um die Umsetzung und Ausführung der einzelnen Komponenten wirklich zu verstehen.



Was ich verwende

  • Raspberry Pi 4
  • Raspberry Pi Kamera Modul 3
  • Visual Studio 2026 oder JetBrains Rider 2025.3.1
  • C# mit .NET 10
  • GitHub inkl. GitHub Actions
  • Linux bzw. Raspbian OS

 


Anforderung

Ziel ist zunächst, auf dem Raspberry Pi einen Dienst zu betreiben, der das aktuell ein "Hello World!" ausgibt.
Auf einem Windows-PC soll mit der Consolen Anwendung der Text mit Datum und Uhrzeit abgerufen werden.

Der gesamte Aufbau soll innerhalb eines privaten Netzwerks funktionieren.

  


Dienstanwendung für Raspberry Pi

Für den Dienst auf dem Raspberry Pi verwenden wir zunächst das Template „Worker Service“ in Visual Studio bzw. „Services“ in Rider.


Neben .NET 10 sind zunächst keine weiteren Einstellungen notwendig. Ich habe jedoch zusätzlich die Option „Do not use top-level statements“ aktiviert – hier bin ich bewusst altmodisch.

Anschließend kann ein passender Projekt- und Solution-Name vergeben werden.

 


Testklasse mit Socket

Damit später eine Konsolenanwendung ein „Hello World!“ inklusive Datum und Uhrzeit empfangen kann, wird die Klasse TcpTestServer.cs angelegt.

 using System.Net;  
 using System.Net.Sockets;  
 using System.Text;  
 namespace CodexzierDirectSight  
 {  
   public static class TcpTestServer  
   {  
     private const int Port = 5000;  
     public static async Task StartAsync(CancellationToken stoppingToken, ILogger logger)  
     {  
       var listener = new TcpListener(IPAddress.Any, Port);  
       listener.Start();  
       logger.LogInformation($"TCP Server listening on port {Port}");  
       try  
       {  
         while (!stoppingToken.IsCancellationRequested)  
         {  
           if (listener.Pending())  
           {  
             var client = await listener.AcceptTcpClientAsync(stoppingToken);  
             _ = HandleClientAsync(client, logger);  
           }  
           else  
           {  
             await Task.Delay(100, stoppingToken); // kleine Pause  
           }  
         }  
       }  
       catch (OperationCanceledException) { } // Normaler Stop  
       finally  
       {  
         listener.Stop();  
       }  
     }  
     private static async Task HandleClientAsync(TcpClient client, ILogger logger)  
     {  
       await using var stream = client.GetStream();  
       var message = Encoding.UTF8.GetBytes($"Hello World! {DateTime.Now:U}\n");  
       await stream.WriteAsync(message, 0, message.Length);  
       client.Close();  
       logger.LogInformation("Sent Hello World to client");  
     }  
   }  
 }  

Anschließend muss in der Worker-Klasse eine Instanz von TcpTestServer eingebunden werden.

 namespace CodexzierDirectSight;  
 public class Worker(ILogger<Worker> logger) : BackgroundService  
 {  
   protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
   {  
     _ = TcpTestServer.StartAsync(stoppingToken, logger);  
     while (!stoppingToken.IsCancellationRequested)  
     {  
       if (logger.IsEnabled(LogLevel.Information))  
       {  
         logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);  
       }  
       await Task.Delay(1000, stoppingToken);  
     }  
   }  
 }  

 


Shell-Script für Deployment

Damit die Installation später per Shell-Eingabe erfolgen kann, wird im Repository-Verzeichnis der Ordner deploy angelegt.



In diesem Ordner befindet sich die Datei install.sh, die im Grunde eine einfache Textdatei ist.

#!/usr/bin/env bash  
 set -e  
 APP_NAME="CodexzierDirectSight"  
 SERVICE_NAME="codexzierdirectsight"  
 INSTALL_DIR="/opt/$SERVICE_NAME"  
 SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"  
 GITHUB_REPO="Codexzier/CodexzierDirectSight"  
 ARCH="linux-arm64"  
 TMP_DIR="/tmp/CodexzierDirectSight-install"  
 echo "== Installing $APP_NAME =="  
 # Root-Check  
 if [ "$EUID" -ne 0 ]; then  
  echo "ERROR: Run as root (use sudo)"  
  exit 1  
 fi  
 # Architektur-Check  
 if [[ "$(uname -m)" != "aarch64" ]]; then  
  echo "ERROR: This installer supports only ARM64"  
  exit 1  
 fi  
 # Vorbereitung  
 rm -rf "$TMP_DIR"  
 mkdir -p "$TMP_DIR"  
 mkdir -p "$INSTALL_DIR"  
 # Latest Release URL ermitteln  
 echo "Fetching latest release info..."  
 DOWNLOAD_URL=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases/latest \  
  | grep browser_download_url \  
  | grep "linux-arm64" \  
  | cut -d '"' -f 4)  
 if [ -z "$DOWNLOAD_URL" ]; then  
  echo "ERROR: Could not find release artifact"  
  exit 1  
 fi  
 # Download  
 echo "Downloading $DOWNLOAD_URL"  
 curl -L "$DOWNLOAD_URL" -o "$TMP_DIR/app.tar.gz"  
 # Stop Service (falls vorhanden)  
 systemctl stop $SERVICE_NAME 2>/dev/null || true  
 # Entpacken  
 tar -xzf "$TMP_DIR/app.tar.gz" -C "$INSTALL_DIR"  
 chmod +x "$INSTALL_DIR/CodexzierDirectSight"  
 # systemd Service installieren  
 cat > "$SERVICE_FILE" <<EOF  
 [Unit]  
 Description=CodexzierDirectSight Worker Service  
 After=network.target  
 [Service]  
 ExecStart=$INSTALL_DIR/CodexzierDirectSight  
 WorkingDirectory=$INSTALL_DIR  
 Restart=always  
 RestartSec=5  
 KillSignal=SIGINT  
 SyslogIdentifier=CodexzierDirectSight  
 User=root  
 [Install]  
 WantedBy=multi-user.target  
 EOF  
 # systemd reload & start  
 systemctl daemon-reexec  
 systemctl daemon-reload  
 systemctl enable $SERVICE_NAME  
 systemctl start $SERVICE_NAME  
 echo "== Installation complete =="  
 systemctl status $SERVICE_NAME --no-pager  

 


GitHub Workflow einrichten

Um die Bereitstellung über GitHub zu ermöglichen, muss ein entsprechender Workflow eingerichtet werden.

name: Release Codexzier DirectSight  
 on:  
  push:  
   tags:  
    - 'release-*.*.*'  
 permissions:  
  contents: write  
 jobs:  
  build-release:  
   runs-on: ubuntu-latest  
   env:  
    APP_NAME: CodexzierDirectSight  
    PROJECT_PATH: CodexzierDirectSight/CodexzierDirectSight.csproj  
    RUNTIME: linux-arm64  
    CONFIGURATION: Release  
   steps:  
   - name: Checkout source   
    uses: actions/checkout@v4  
   - name: Setup .NET  
    uses: actions/setup-dotnet@v4  
    with:  
     dotnet-version: 10.0.x  
   - name: Restore dependencies  
    run: dotnet restore $PROJECT_PATH  
   - name: Publish (self-contained)  
    run: |  
      dotnet publish $PROJECT_PATH \  
       -c $CONFIGURATION \  
       -r $RUNTIME \  
       --self-contained true \  
       /p:PublishSingleFile=true \  
       /p:PublishTrimmed=false \  
       -o publish  
   - name: Prepare artifact  
    run: |  
      mkdir -p artifact  
      cp -r publish/* artifact/  
      echo "${GITHUB_REF_NAME}" > artifact/VERSION  
      tar -czf codexzierdirectsight-linux-arm64.tar.gz -C artifact .  
   - name: Create GitHub Release  
    uses: softprops/action-gh-release@v2  
    with:  
      files: codexzierdirectsight-linux-arm64.tar.gz  
      name: Release ${{ github.ref_name }}  
      generate_release_notes: true  


Bereitstellung

Die Bereitstellung ist so konzipiert, dass ein neuer Tag im GitHub-Repository erstellt werden muss.


Anschließend auf „Draft new release“ klicken.


Der Name des Tags muss der im Workflow definierten Konvention entsprechen:

release-..*


Danach Titel und Beschreibung für das Release ergänzen und auf „Publish Release“ klicken.


Wurde der Tag korrekt gesetzt, startet automatisch der Workflow. Unter Actions sollte ein neuer Eintrag sichtbar sein. Der orangefarbene, animierte Punkt signalisiert, dass der Build aktuell ausgeführt wird.


Nach wenigen Sekunden wechselt der Status auf Grün, und die Bereitstellung kann auf dem Raspberry Pi mit Raspbian OS abgerufen werden. 


Installation auf dem Raspberry Pi

Hinweis: Der Code Stand hat sich bereits verändert und sollte bereits nicht mehr das "Hello World" zurück geben. Siehe am Ende den Link zu dem Branch, welches den Version Stand zu diesen Blogpost wiedergibt.

Mit folgendem Befehl wird die Installation ausgeführt und der Dienst gestartet:

curl -sSL https://raw.githubusercontent.com/Codexzier/CodexzierDirectSight/master/deploy/install.sh | sudo bash 

 


Testen

Prüfen, ob der Dienst nach der Installation läuft:

 systemctl status codexzierdirectsight

Wichtig ist, dass der Status „active (running)“ in grüner Schrift angezeigt wird. Dann ist sichergestellt, dass der Dienst korrekt läuft.

Ausgabe der aktuellen Log-Einträge anzeigen (Abbruch mit STRG + C):

journalctl -u codexzierdirectsight -f

 


Hello World empfangen

Mit einer Konsolenanwendung unter Windows soll nun das „Hello World!“ inklusive Datum empfangen werden.
Dazu wird eine neue C#-Konsolenanwendung erstellt und folgender Code eingefügt:

internal class Program  
 {  
   static async Task Main(string[] args)  
   {  
     string raspberryIp = "192.168.178.88"; // IP Adresse of Raspberry Pi  
     int port = 5000;  
     using var client = new TcpClient();  
     await client.ConnectAsync(raspberryIp, port);  
     using var stream = client.GetStream();  
     byte[] buffer = new byte[1024];  
     int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);  
     string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);  
     Console.WriteLine($"Received from server: {message}");  
   }  
 } 

Nach dem Start sollte folgendes Ergebnis erscheinen:



Rückblick

Rückblickend muss ich sagen: Ohne ChatGPT hätte ich für das Installations-Shell-Script und den YAML-Workflow deutlich länger gebraucht. Vor allem das Verständnis der benötigten Befehle und des Zusammenspiels der einzelnen Schritte hätte mich viel Zeit gekostet.

Über klassische Google-Suchen finde ich solche Lösungen nur schwer – entweder fehlen mir die richtigen Schlagwörter oder die gesponserten Ergebnisse erschweren es, schnell zu beurteilen, ob eine Seite überhaupt relevant ist.

Codestand habe ich im ersten Branch abgelegt, dass zu dem Blogpost gehört:

Codexzier/CodexzierDirectSight at Create-Initialize-RaspberryPi-Service

Donnerstag, 6. Februar 2025

Spiele Entwicklung: Zurück zu MonoGame


In den Anfangszeiten wurde über die Google-Suche über den Begriff oft Ergebnisse zu Monogamie und Beziehung ausgegeben. Das hat sich inzwischen geändert und durch eine Artikelserie in der .NET Pro wurde mein Interesse wieder geweckt.

Grund meines Interesses liegt mit unter wieder über den Horizont zu schauen, weil Unity 3D vorletztes Jahr (2023) mit einen zweifelhaften Geschäftsmodel kam. Also war mein Ausweichen zunächst auf Unreal Engine und wegen fehlender Zeit kam ich zunächst nicht dazu.

Zwischen Weihnachten und Sylvester hatte ich endlich etwas Zeit und las die ersten Teile der Artikelserie in der .NET Pro und fing dann an wieder zu Entwickeln mit dem MonoGame Framework für Spiele Entwicklung.


Was braucht man für MonoGame?


Welches Ziel verfolge ich?

Spielideen und Spielmechaniken ausprobieren in einem kleineren und eingeschränkten Rahmen aufbauen.


Warum ein Map Editor?

Eines meiner Lernziele ist das Erstellen einer eigenen Spielkarte, auf dem ich meine Aufgaben und Ziele hinterlegen kann. Dazu gehört auch das Programmieren eines Tools, mit dem ich schnell einen Karten Abschnitt mit Aufgaben anlegen kann.


Wie geht’s weiter?

Zeitlich bin ich oft vollbeschäftigt. Aber hin und wieder komme ich schrittweise weiter und abgesehen davon macht das Programmieren spaß.


Repository: Codexzier/MiniShipDelivery

 

Fingerübungen - MS SQL Pivot

Nach drei Jahren MS SQL und ich finde mal wieder etwas, dass ich noch nicht kenne und ich auch nicht in der Schulung hatte. 

Bis vor heute, war mir nicht bewusst, dass man auch Pivot Ergebnisse in MS SQL schreiben kann.

Beispiel: Ich habe in der einen Spalte, mehrere verschiedene Themen, die ich hier Unkreativ "Sache 1" bis "Sache 3" genannt habe. Diese lassen sich am Ende so raus führen, dass am Ende daraus drei Spalten werden. Hätte ich das vorher gewusst, hätte ich viel bessere Abfragen schreiben können.

 DECLARE @myTable TABLE (Dinge VARCHAR(10), Menge INT)  
 INSERT INTO @myTable  
 (  
   Dinge,  
   Menge  
 )  
 VALUES  
 ( 'Sache 1', 10 ),  
 ( 'Sache 1', 2 ),  
 ( 'Sache 1', 5 ),  
 ( 'Sache 1', 77 ),  
 ( 'Sache 2', 10 ),  
 ( 'Sache 2', 1 ),  
 ( 'Sache 2', 2 ),  
 ( 'Sache 2', 3 ),  
 ( 'Sache 3', 10 ),  
 ( 'Sache 3', 10 ),  
 ( 'Sache 3', 10 )  
 SELECT [pvt].[Sache 1], pvt.[Sache 2], pvt.[Sache 3]  
 FROM @myTable t  
 PIVOT  
 (  
 SUM(Menge)  
 FOR Dinge IN([Sache 1], [Sache 2], [Sache 3])  
 ) AS pvt  

Ameisen Simulation und andere Dinge

Wer lange sich schon mit C# beschäftig, hat sicherlich schon mal was von AntMe gelesen oder gehört. Diese Idee hatte ich aufgegriffen und mi...