[{"content":" Status: Informelles Referenzdokument / Pattern-Beschreibung\nZielgruppe: Linux-Administratoren, DevOps-/Platform-Engineers, systemd- und Podman-Anwender\nBeispiel-Implementierung: Reactive-PlantUML auf GitHub\nMotivation Moderne Entwickler- und Administrationsumgebungen benötigen zunehmend lokale Dienste, die:\nreproduzierbar konfiguriert sind, keine permanent laufenden Hintergrundprozesse erfordern, ohne Root-Rechte betrieben werden können, und dennoch sauber in bestehende Systemmechanismen integriert sind. Typische Beispiele sind lokale Vorschau‑Server, Dokumentationsdienste oder Hilfsdienste für IDEs.\nReactive-PlantUML entstand aus der Beobachtung, dass systemd, UNIX-Domain-Sockets und rootlose Container bereits alle notwendigen Bausteine bereitstellen, um solche Dienste reaktiv und bedarfsorientiert zu betreiben – ohne zusätzliche Infrastruktur oder Spezialframeworks.\nDieses Dokument beschreibt das daraus abgeleitete Betriebsmuster, nicht die konkrete PlantUML-Implementierung.\nGrundprinzipien des Musters Das hier beschriebene Muster basiert auf folgenden Invarianten:\nRootless by default Alle Komponenten laufen als systemd user services.\nSocket-Aktivierung statt Daemons Dienste starten ausschließlich bei tatsächlicher Nutzung.\nExplizite Port-Verantwortung Die Zuordnung von TCP-Ports erfolgt bewusst durch den Benutzer.\nKeine internen TCP-Ports Interne Kommunikation erfolgt ausschließlich über UNIX-Domain-Sockets.\nAutomatisches Idle-Shutdown Dienste beenden sich nach Inaktivität selbstständig.\nReproduzierbarkeit vor Magie Alle Abhängigkeiten und Artefakte sind versioniert und überprüfbar.\nArchitekturüberblick Der Datenfluss folgt einem strikt linearen Modell:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Client (Browser, IDE, CLI) │ │ TCP (lokaler, benutzerdefinierter Port) ▼ systemd .socket (User-Unit) │ ▼ systemd-socket-proxyd │ │ UNIX-Domain-Socket (%t/plantuml/\u0026lt;port\u0026gt;.sock) ▼ rootloser Podman-Container │ ▼ Applikation (z. B. PlantUML HTTP-Server) Wesentliche Beobachtung:\nKein Bestandteil kennt mehr Kontext als notwendig.\nRolle der einzelnen Komponenten systemd Socket Units (User) Definieren ausschließlich den extern sichtbaren TCP-Port Starten keinerlei Logik Der Instanzname entspricht direkt dem Port (plantuml@8080.socket) Dies erzwingt eine explizite, konfliktfreie Portwahl durch den Benutzer.\nsystemd-socket-proxyd Übernimmt die Brückenfunktion zwischen TCP und UNIX-Socket Wartet implizit auf die Verfügbarkeit des Ziel-Sockets Beendet sich automatisch nach Inaktivität (--exit-idle-time) Wichtig:\nsystemd-socket-proxyd ist zustandslos und enthält keinerlei Anwendungslogik.\nPodman-Container (Quadlet) Der Container:\nläuft rootlos besitzt keinen exponierten TCP-Port erzeugt und verwaltet ausschließlich einen UNIX-Domain-Socket Der Socket wird über ein RuntimeDirectory bereitgestellt und per Volume in den Container gemountet.\nInterne Socket-Brücke (socat) Innerhalb des Containers:\nstellt die Applikation lokal einen TCP-Port bereit socat bridged diesen Port zu einem UNIX-Socket Dieses Detail ist austauschbar und kein Bestandteil des Musters – entscheidend ist lediglich, dass:\ndie Container-Grenze ausschließlich über einen UNIX-Socket überschritten wird.\nHealth Checks und Zustandsmodell Health Checks erfolgen:\nüber den UNIX-Socket ohne Nutzung externer Ports sowohl während des Starts als auch im laufenden Betrieb Der Container meldet seine Bereitschaft explizit an systemd (Notify=healthy).\nDamit entsteht ein klares Zustandsmodell:\nSocket offen → Anfrage möglich Container healthy → Dienst verfügbar Idle → automatisches Herunterfahren Installation und Betrieb Das Muster ist distributionsneutral, lässt sich aber besonders gut über Debian-Pakete ausrollen:\nsystemd-User-Units Podman-Quadlets Container-Build-Dateien Nach Installation ist keine Aktivität erforderlich, bis der Benutzer bewusst einen Socket aktiviert:\n1 systemctl --user enable --now plantuml@8080.socket Abgrenzung und bewusste Nicht-Ziele Dieses Muster versucht ausdrücklich nicht:\nklassische Service-Orchestrierung zu ersetzen Netzwerk-Namensräume zu abstrahieren bestehende Init-Systeme zu umgehen neue Sicherheitsmodelle einzuführen Es nutzt ausschließlich:\nbestehende systemd-Mechanismen etablierte UNIX-Prinzipien klar definierte Verantwortlichkeiten Übertragbarkeit des Musters Obwohl Reactive-PlantUML der konkrete Anwendungsfall ist, eignet sich das Muster u. a. für:\nlokale Preview-Server Dokumentationsdienste Entwicklungs-Backends Hilfsdienste für IDEs kurzlebige Werkzeuge mit Netzwerkbedarf Die konkrete Applikation ist austauschbar.\nFazit Reactive-PlantUML zeigt kein neues Werkzeug, sondern ein ruhiges Zusammenspiel bestehender Mechanismen.\nDas Muster ist:\ntransparent reproduzierbar wartbar und bewusst unspektakulär Oder anders formuliert:\nEs passiert nur genau das, was passieren muss – und nichts darüber hinaus.\n","date":"2025-12-18T00:00:00Z","image":"https://0xMax42.io/p/reaktive-lokale-dienste-mit-systemd-unix-sockets-und-rootlosen-containern/reactive-services_hu_8961bfb700079fc.webp","permalink":"https://0xMax42.io/p/reaktive-lokale-dienste-mit-systemd-unix-sockets-und-rootlosen-containern/","title":"Reaktive lokale Dienste mit systemd, UNIX-Sockets und rootlosen Containern"},{"content":"Es gibt diese kleinen Probleme, die einen jahrelang unterschwellig nerven, bis man sie endlich einmal sauber löst. Genau in diese Kategorie fällt bei mir das Thema „Was macht das System, wenn ich auf Akku bin – und was darf es nur am Netz?“\nKlar, es gibt Tools wie TLP, Powertop \u0026amp; Co., und viele Desktop-Umgebungen haben ihre eigenen Energiespar-Profile. Aber ich wollte etwas anderes:\nIch wollte den aktuellen Power-Zustand (Akku vs. Netz) als systemd-Target haben – sauber integriert, automatisierbar, paketiert und über meine eigene Debian-Repository-Infrastruktur ausrollbar.\nDas Ergebnis ist dieses kleine, aber sehr praktische Projekt: power-state-target.\nProblem: Der Energiezustand als „unsichtbare“ Information UPower weiß, ob die Maschine auf Akku läuft. Der Desktop weiß es. Der Nutzer sieht irgendwo ein Batteriesymbol. Aber für viele meiner eigenen Dienste, Timer und Skripte ist diese Info nicht direkt greifbar.\nTypische Fragen:\n„Starte diesen Dienst nur, wenn das Gerät am Netz hängt.“ „Fahre diese IO-intensiven Jobs bitte nicht auf Akku.“ „Auf Akku darf etwas weiterlaufen, aber nur in einem Light-Mode.“ Man kann natürlich überall lokale Checks einbauen – upower, busctl, eigene Daemons, irgendwelche kleinen Wrapper. Aber das ist alles verteilt, unübersichtlich und schlecht testbar.\nSystemd bietet für solche Zustände eigentlich eine perfekte Abstraktion: Targets.\nAlso war der Plan:\nIch möchte ein power-ac.target und ein power-battery.target haben. Genau einer dieser Targets soll immer „aktiv“ sein – je nachdem, ob das System auf Akku oder am Netz hängt. Dienste können sich dann einfach daran „dranhängen“, statt selbst Power-Logik zu implementieren. Ziel: systemd als Single Source of Truth für den Power-State Die Idee hinter power-state-target ist bewusst minimalistisch:\nKeine eigene fette Daemon-Logik. Nutzung von vorhandener Infrastruktur: UPower und systemd. Ein kleiner Listener, der den Zustand aus UPower liest und passende systemd-Targets setzt. Das Ganze als Debian-Paket, das sich in meine bestehende Build- und Release-Infrastruktur einfügt. Im Kern besteht das Projekt aus:\nzwei systemd-Targets: power-ac.target und power-battery.target einem systemd-Service: power-state-listener.service einem kleinen Shell-Skript: /usr/libexec/update-power-state Debian-Packaging + automatisierter Build- und Release-Pipeline über Gitea Die Targets: power-ac und power-battery Die beiden Targets sind bewusst extrem simpel gehalten. Sie dienen als Zustandsmarkierungen, nicht als eigene Logik-Einheiten:\n1 2 3 4 # /usr/lib/systemd/system/power-ac.target [Unit] Description=AC Power Mode AllowIsolate=yes 1 2 3 4 # /usr/lib/systemd/system/power-battery.target [Unit] Description=Battery Power Mode AllowIsolate=yes Ein paar Gedanken dazu:\nSie haben keine eigenen [Install]-Sektionen – sie werden nicht direkt aktiviert, sondern vom Listener-Service gesetzt. Über AllowIsolate=yes kann man sie theoretisch auch isolieren, falls man das einmal für Experimente nutzen möchte. Für andere Units sind sie vor allem als Anker gedacht: Wants=power-ac.target, BindsTo=power-battery.target, PartOf= usw. Die eigentliche Intelligenz steckt im Listener.\nDer Listener: power-state-listener.service Der Listener ist ein kleiner systemd-Service, der genau eine Aufgabe hat:\nUPower beobachten und bei Änderungen der Energiequelle das passende Target aktivieren.\nDie Unit dazu sieht so aus:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # /usr/lib/systemd/system/power-state-listener.service [Unit] Description=Wait for UPower power-state change and update targets Wants=upower.service After=upower.service [Service] Type=simple ExecStartPre=/usr/libexec/update-power-state ExecStart=/bin/sh -c \\ \u0026#39;upower --monitor-detail | head -n 1 \u0026gt; /dev/null; \\ /usr/libexec/update-power-state\u0026#39; Restart=on-success RestartSec=0 [Install] WantedBy=multi-user.target Die wichtigen Punkte:\nExecStartPre=/usr/libexec/update-power-state sorgt dafür, dass der aktuelle Zustand sofort beim Start des Dienstes gesetzt wird (z.B. nach einem Boot). ExecStart=... upower --monitor-detail hängt sich an die UPower-Monitor-API und wartet auf ein Ereignis (zum Beispiel: Stromkabel gezogen oder eingesteckt). Sobald ein Ereignis eintritt, läuft danach erneut update-power-state, dann beendet sich der Dienst erfolgreich. Durch Restart=on-success wird der Service automatisch neu gestartet. Ergebnis: eine kleine Ereignis-Schleife, die immer reagiert, wenn sich der Power-State ändert, ohne selbst busy zu sein. Das ist im Grunde ein reaktives Mini-Framework: systemd + UPower erledigen 95 % der Arbeit, der Rest ist eine dünne Klebeschicht.\nDie eigentliche Logik: /usr/libexec/update-power-state Das Herzstück ist ein Shell-Skript, das genau zwei Dinge tut:\nDen aktuellen UPower-Status („bin ich auf Akku?“) abfragen. Die passenden systemd-Targets setzen. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/bin/sh get_state() { busctl get-property org.freedesktop.UPower \\ /org/freedesktop/UPower \\ org.freedesktop.UPower \\ OnBattery \\ | awk \u0026#39;{print $2}\u0026#39; } apply_state() { state=\u0026#34;$1\u0026#34; if [ \u0026#34;$state\u0026#34; = \u0026#34;true\u0026#34; ]; then systemctl start power-battery.target systemctl stop power-ac.target else systemctl start power-ac.target systemctl stop power-battery.target fi } apply_state \u0026#34;$(get_state)\u0026#34; Ein paar Details dazu:\nbusctl get-property ... OnBattery holt sich direkt den OnBattery-Property von UPower über D-Bus. UPower liefert hier ein b true oder b false – über awk '{print $2}' ziehe ich mir einfach den Wahrheitswert heraus. Wenn true, wird power-battery.target gestartet und power-ac.target gestoppt. Wenn false, genau umgekehrt. Damit ist sichergestellt, dass nie beide Targets gleichzeitig aktiv sind, sondern immer genau einer der beiden Zustände gilt.\nIntegration in Debian: Paket power-state-target Weil ich praktisch alles, was ich produktiv nutzen will, in .deb-Pakete verpacke, war von Anfang an klar: Das hier wird ein normales Debian-Paket mit sauberem debian/-Verzeichnis.\nDie debian/control ist bewusst minimal:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Source: power-state-target Section: utils Priority: optional Maintainer: 0xMax42 \u0026lt;debian@0xMax42.io\u0026gt; Build-Depends: debhelper-compat (= 13) Standards-Version: 4.7.0 Rules-Requires-Root: no Package: power-state-target Architecture: all Depends: ${misc:Depends}, upower Description: Power state target management Introduces a systemd target that reflects the current power state of the system (on battery, on AC power) and updates it dynamically as the power state changes. Architektur ist all, weil es sich nur um Skripte und Unit-Files handelt. Die einzige echte Laufzeit-Abhängigkeit ist upower. Rules-Requires-Root: no – gebaut werden kann also auch ohne Root, sofern die Build-Dependencies vorhanden sind. Die Installation der Dateien übernimmt debian/install:\n1 2 3 4 usr/libexec/update-power-state usr/libexec/ usr/lib/systemd/system/power-state-listener.service usr/lib/systemd/system/ usr/lib/systemd/system/power-ac.target usr/lib/systemd/system/ usr/lib/systemd/system/power-battery.target usr/lib/systemd/system/ debian/rules ist dementsprechend trivial:\n1 2 3 4 #!/usr/bin/make -f %: dh $@ Das Ganze ist als native Paketquelle (debian/source/format = 3.0 (native)) angelegt, da Projekt und Paket faktisch identisch sind.\nAutomatisierter Debian-Build mit build.sh Um das Paket nicht nur lokal, sondern auch automatisiert im CI bauen zu können, gibt es ein eigenes Build-Skript:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #!/bin/bash set -euo pipefail REQUIRED_PKGS=(build-essential devscripts dkms debhelper dh-dkms) missing=() for pkg in \u0026#34;${REQUIRED_PKGS[@]}\u0026#34;; do dpkg -s \u0026#34;$pkg\u0026#34; \u0026amp;\u0026gt;/dev/null || missing+=(\u0026#34;$pkg\u0026#34;) done if (( ${#missing[@]} )); then echo \u0026#34;🔧 Installing missing build packages: ${missing[*]}\u0026#34; sudo apt-get update -qq sudo apt-get install -y --no-install-recommends \u0026#34;${missing[@]}\u0026#34; fi DIST_DIR=\u0026#34;dist\u0026#34; TAG=\u0026#34;${TAG_NAME:-$(exit 1)}\u0026#34; echo \u0026#34;🔖 Using tag: $TAG\u0026#34; curl -s https://git.0xmax42.io/actions/deb-changelog-action/raw/branch/main/bootstrap.sh | bash -s -- \\ --version \u0026#34;v0\u0026#34; \\ --tag \u0026#34;$TAG\u0026#34; \\ --package_name \u0026#34;power-state-target\u0026#34; \\ --author_name \u0026#34;0xMax42\u0026#34; \\ --author_email \u0026#34;Mail@0xMax42.io\u0026#34; PKG_NAME=$(dpkg-parsechangelog --show-field Source) PKG_VERSION=$(dpkg-parsechangelog --show-field Version) tar --exclude=debian -czf ../power-state-target_${PKG_VERSION}.orig.tar.gz . echo \u0026#34;Building Debian package...\u0026#34; dpkg-buildpackage -us -uc mkdir -p \u0026#34;$DIST_DIR\u0026#34; rm -rf \u0026#34;$DIST_DIR\u0026#34;/* for file in ../${PKG_NAME}*_${PKG_VERSION}_*.deb \\ ../${PKG_NAME}*_${PKG_VERSION}_*.ddeb \\ ../${PKG_NAME}*_${PKG_VERSION}_*.buildinfo \\ ../${PKG_NAME}*_${PKG_VERSION}_*.changes \\ ../${PKG_NAME}*_${PKG_VERSION}.dsc \\ ../${PKG_NAME}*_${PKG_VERSION}.*.tar.* \\ ../${PKG_NAME}*_${PKG_VERSION}.tar.*; do [[ -f \u0026#34;$file\u0026#34; ]] \u0026amp;\u0026amp; { mv \u0026#34;$file\u0026#34; \u0026#34;$DIST_DIR/\u0026#34;; echo \u0026#34;📦 Moved $(basename \u0026#34;$file\u0026#34;) → $DIST_DIR/\u0026#34;; } done \u0026#34;dpkg-buildpackage\u0026#34; -T clean echo \u0026#34;Build complete. Output in $DIST_DIR/\u0026#34; Wichtige Punkte:\nDas Script kümmert sich darum, dass alle benötigten Build-Pakete installiert sind (typische build-essential/devscripts/debhelper-Kette). Die Versionierung und der Changelog werden über ein externes Script aus meiner deb-changelog-action automatisiert. Alle entstehenden Artefakte werden in ein zentrales dist/-Verzeichnis verschoben, sodass der CI-Job sie leicht weiterverarbeiten kann. TAG_NAME kommt dabei aus dem CI-Kontext – das ist die Git-Tag-Version, die zum Release gehört.\nCI/CD: Vom Commit zum Debian-Paket in der eigenen Repo Da das Projekt auf meiner privaten Gitea-Instanz liegt, laufen die CI-Pipelines ebenfalls dort. Es gibt zwei wesentliche Workflows:\nrelease.yml – erstellt automatisch Releases und Changelogs. build.yml – baut aus einem Release ein Debian-Paket und schiebt es in meine debrepo. Auto-Changelog \u0026amp; Release Der Release-Workflow triggert bei Pushes auf main:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 on: push: branches: - main jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Release uses: https://git.0xmax42.io/actions/auto-changelog-release-action@v1 with: token: ${{ secrets.RELEASE_PUBLISH_TOKEN }} Die auto-changelog-release-action kümmert sich darum, anhand der Commits (per git-cliff-Konfiguration) einen Changelog zu erzeugen, eine neue Version zu bestimmen, den Tag zu setzen und das Release im Gitea-Repository zu veröffentlichen.\nDebian-Build und Push in debrepo Sobald ein Release veröffentlicht ist, greift der zweite Workflow:\n1 2 3 on: release: types: [published] Der Job:\ncheckt den Quellcode aus, ruft build.sh mit dem Tag aus dem Release auf, macht ein Sparse Checkout meiner debrepo (nur power-state-target/), kopiert die .deb, .dsc und Tarballs in dieses Verzeichnis, commitet neue Artefakte auf den packages-Branch. Damit landet jede neue Version von power-state-target automatisch in meiner eigenen APT-Repository-Struktur – und kann dann wie gewohnt über apt installiert werden.\ngit-cliff: Konsistente Changelogs mit Emojis Da ich inzwischen sehr viel Wert auf saubere, konsistente Changelogs lege, kommt auch hier wieder git-cliff zum Einsatz. Die cliff.toml ist an meinen üblichen Stil angepasst – inklusive Emoji-Gruppierung:\n🚀 Features 🐛 Bug Fixes ⚙️ Miscellaneous Tasks usw. Für power-state-target sieht der erste Release-Eintrag aktuell so aus:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ## [0.1.0] - 2025-12-02 ### 🚀 Features - Add systemd targets and service for dynamic power state switching - (...) ### 🐛 Bug Fixes - *(config)* Correct systemd installation paths in debian install - (...) ### ⚙️ Miscellaneous Tasks - *(ci)* Add build and release workflows for package automation - (...) - *(build)* Add script for automated Debian package build - (...) - Add git-cliff changelog configuration file - (...) - *(debian)* Add packaging files for power-state-target - (...) Nichts weltbewegendes, aber genau das ist der Punkt: saubere kleine Bausteine, die später zusammenspielen können.\nWie man power-state-target praktisch nutzt Der eigentliche Mehrwert entsteht dadurch, dass andere Units auf die Targets reagieren können.\nEin paar typische Szenarien:\n1. Dienste nur am Netz erlauben Angenommen, du hast einen Dienst, der nur laufen soll, wenn das System am Netz hängt, z.B. ein Backup- oder Dedupe-Job.\n1 2 3 4 5 6 7 8 9 10 11 12 [Unit] Description=Heavy IO Job (AC only) Wants=power-ac.target After=power-ac.target [Service] Type=oneshot ExecStart=/usr/local/bin/heavy-job.sh [Install] WantedBy=multi-user.target WantedBy=power-ac.target Hier kann man zusätzlich mit ConditionPathExists, Timer-Units, StartLimit* usw. arbeiten – aber der Kern ist: „nur sinnvoll, wenn AC da ist“.\n2. Reduzierter Modus auf Akku Statt Dienste komplett abzuschalten, kann man auch alternative Konfigurationen fahren, z.B. einen „Light-Modus“ auf Akku und „Full-Modus“ am Netz. Man modelliert dann zwei Units, die jeweils an ein anderes Target gekoppelt sind.\n3. Timer, die auf den Power-State hören Timer-Units können ebenfalls indirekt über die Targets gesteuert werden, etwa indem nur der eigentliche Service an das passende Target gebunden wird.\nDas Schöne: Alle Entscheidungen laufen über systemd, nicht über wilde Shell-Ifs überall im System.\nAusblick: Was man darauf noch aufbauen kann power-state-target ist bewusst klein gehalten. Aber genau dadurch lässt es sich gut als Baustein für komplexere Setups verwenden:\nDynamisches Umschalten des CPU-Governors über separate Dienste. Anpassung von Backup-Strategien (z.B. nur Metadaten auf Akku, Voll-Backups am Netz). Steuerung von Container- oder VM-Last abhängig vom Power-State. Integration in bestehende Tuning-Tools, indem diese die Targets nur noch als Signalquelle verwenden. Ich mag solche Projekte, weil sie zwei Welten verbinden:\nAuf der einen Seite: klassische Desktop-/Laptop-Welt mit Akku, UPower, grafischen Energiemanagern. Auf der anderen: systemd- und serverartige Steuerung, bei der Zustände explizit modelliert sind. power-state-target macht den Energiezustand zu einem ersten Bürger in systemd – und damit für alles Weitere komponierbar.\nFazit Für sich genommen ist power-state-target ein kleines Projekt:\nein Shell-Skript, zwei Targets, ein Service, etwas Debian-Packaging und CI. Aber für mich ist es genau die Art von Infrastruktur, die ich mag:\nklar umrissen, gut integrierbar, automatisiert gebaut und ausgeliefert, und mit einem einfachen mentalen Modell: „Bin ich auf Akku oder am Netz?“ wird zu „Welches systemd-Target ist aktiv?“ Und ab da kann der Rest des Systems sich darauf verlassen.\n","date":"2025-12-02T21:30:00Z","image":"https://0xMax42.io/p/power-state-target-wie-ich-den-energiezustand-meines-laptops-in-systemd-modelliert-habe/cover_hu_1a1efbd9f3c9b9a7.webp","permalink":"https://0xMax42.io/p/power-state-target-wie-ich-den-energiezustand-meines-laptops-in-systemd-modelliert-habe/","title":"power-state-target: Wie ich den Energiezustand meines Laptops in systemd modelliert habe"},{"content":"Mit meinem Artikel \u0026ldquo;Minimaler Skidbuffer mit AXI-like Handshaking\u0026rdquo; habe ich bereits die Grundlage gelegt. Jetzt habe ich das ganze in Aktion zusammen mit meinen AXI-Pipeline Modulen auf einem älteren Spartan-3E FPGA getestet.\nZiel war es eine Art \u0026ldquo;Performance-Benchmark\u0026rdquo; zu erstellen, um die Effizienz des Skidbuffers in einer realen AXI-Pipeline zu demonstrieren. Dafür habe ich ein AXI-Pipeline Modul erstellt, welches dynamisch zwischen \u0026ldquo;Mit Skidbuffer\u0026rdquo; und \u0026ldquo;Ohne Skidbuffer\u0026rdquo; umschalten kann. Das ermöglicht es, die Auswirkungen des Skidbuffers auf die erreichbare Taktfrequenz zu messen.\nAXI-Pipeline Module Folgend der Code des AXI-Pipeline Moduls, das den Skidbuffer integriert:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.all; entity Pipeline_pb_Module is generic ( --@ Number of pipeline stages G_PipelineStages : integer := 10; --@ Data width G_Width : integer := 32; --@ Register balancing attribute\u0026lt;br\u0026gt; --@ - \u0026#34;no\u0026#34; : No register balancing \u0026lt;br\u0026gt; --@ - \u0026#34;yes\u0026#34;: Register balancing in both directions \u0026lt;br\u0026gt; --@ - \u0026#34;forward\u0026#34;: Moves a set of FFs at the inputs of a LUT to a single FF at its output. \u0026lt;br\u0026gt; --@ - \u0026#34;backward\u0026#34;: Moves a single FF at the output of a LUT to a set of FFs at its inputs. G_RegisterBalancing : string := \u0026#34;no\u0026#34;; --@ Enable pipeline buffer --@ - true : Use pipeline buffer --@ - false : Direct connection (bypass) G_EnablePipelineBuffer : boolean := false ); port ( I_CLK : in std_logic; I_RST : in std_logic; I_CE : in std_logic; --- I_Data : in std_logic_vector(G_Width - 1 downto 0); I_Valid : in std_logic; O_Ready : out std_logic; --- O_Data : out std_logic_vector(G_Width - 1 downto 0); O_Valid : out std_logic; I_Ready : in std_logic ); end entity Pipeline_pb_Module; architecture RTL of Pipeline_pb_Module is signal C_PipelineEnable : std_logic; signal C_PipelineBufferEnable : std_logic_vector(1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); signal R_Valid : std_logic; signal R_Ready : std_logic; signal R_Data : std_logic_vector(G_Width - 1 downto 0); signal C_Data : std_logic_vector(G_Width - 1 downto 0); begin INST_PipelineControllerIn : entity work.PipelineController generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_ResetActiveAt =\u0026gt; \u0026#39;1\u0026#39; ) port map( I_CLK =\u0026gt; I_CLK, I_RST =\u0026gt; I_RST, I_CE =\u0026gt; I_CE, O_Enable =\u0026gt; C_PipelineEnable, I_Valid =\u0026gt; I_Valid, O_Ready =\u0026gt; O_Ready, O_Valid =\u0026gt; R_Valid, I_Ready =\u0026gt; R_Ready ); INST_PipelineRegisterIn : entity work.PipelineRegister generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_Width =\u0026gt; G_Width, G_RegisterBalancing =\u0026gt; G_RegisterBalancing ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; C_PipelineEnable, I_Data =\u0026gt; I_Data, O_Data =\u0026gt; R_Data ); --------- C_Data \u0026lt;= std_logic_vector(unsigned(R_Data) + 3); -- Example operation, can be replaced with actual logic --------- -- Pipeline Buffer Generation based on G_EnablePipelineBuffer GEN_PipelineBuffer : if G_EnablePipelineBuffer generate INST_PipelineBufferController : entity work.PipelineBufferController generic map( G_ResetActiveAt =\u0026gt; \u0026#39;1\u0026#39; ) port map( I_CLK =\u0026gt; I_CLK, I_RST =\u0026gt; I_RST, I_CE =\u0026gt; I_CE, O_Enable =\u0026gt; C_PipelineBufferEnable, I_Valid =\u0026gt; R_Valid, O_Ready =\u0026gt; R_Ready, O_Valid =\u0026gt; O_Valid, I_Ready =\u0026gt; I_Ready ); INST_PipelineBuffer : entity work.PipelineBuffer generic map( G_Width =\u0026gt; G_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; C_PipelineBufferEnable, I_Data =\u0026gt; C_Data, O_Data =\u0026gt; O_Data ); end generate GEN_PipelineBuffer; -- Direct connection when pipeline buffer is disabled GEN_PassthroughWithoutBuffer : if not G_EnablePipelineBuffer generate O_Valid \u0026lt;= R_Valid; O_Data \u0026lt;= R_Data; R_Ready \u0026lt;= I_Ready; end generate GEN_PassthroughWithoutBuffer; end architecture RTL; Performance Benchmark Das Modul habe ich dann in einem Performance-Benchmark (Synthesefähig) getestet, um die erreichbare Taktfrequenz zu messen. Dabei konnte ich eine maximale Taktfrequenz von 270 MHz erreichen - mit bis zu 250 Modulen in einer Pipeline.\nDer Performance-Benchmark sieht wie folgt aus:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.all; entity Pipeline_pb is generic ( --@ Number of pipeline stages inside each module G_PipelineStages : integer := 2; --@ Data width G_Width : integer := 8; --@ Register balancing attribute\u0026lt;br\u0026gt; --@ - \u0026#34;no\u0026#34; : No register balancing \u0026lt;br\u0026gt; --@ - \u0026#34;yes\u0026#34;: Register balancing in both directions \u0026lt;br\u0026gt; --@ - \u0026#34;forward\u0026#34;: Moves a set of FFs at the inputs of a LUT to a single FF at its output. \u0026lt;br\u0026gt; --@ - \u0026#34;backward\u0026#34;: Moves a single FF at the output of a LUT to a set of FFs at its inputs. G_RegisterBalancing : string := \u0026#34;yes\u0026#34;; --@ Enable pipeline buffer --@ - true : Use pipeline buffer --@ - false : Direct connection (bypass) G_EnablePipelineBuffer : boolean := true; --@ How many Pipeline modules shall be chained? G_PipelineModules : integer := 250; --@ Enable chip enable signal G_Enable_CE : boolean := false; --@ Enable reset signal G_Enable_RST : boolean := false ); port ( I_CLK : in std_logic; I_RST : in std_logic; I_CE : in std_logic; --- I_Data : in std_logic_vector(G_Width - 1 downto 0); I_Valid : in std_logic; O_Ready : out std_logic; --- O_Data : out std_logic_vector(G_Width - 1 downto 0); O_Valid : out std_logic; I_Ready : in std_logic ); end entity Pipeline_pb; architecture RTL of Pipeline_pb is --------------------------------------------------------------------------- -- Attribute helpers --------------------------------------------------------------------------- attribute keep : string; attribute IOB : string; --------------------------------------------------------------------------- -- Bench‐wrapper FFs (synchronous IO) --------------------------------------------------------------------------- signal R_RST : std_logic := \u0026#39;0\u0026#39;; signal R_CE : std_logic := \u0026#39;1\u0026#39;; attribute keep of R_RST, R_CE : signal is \u0026#34;true\u0026#34;; attribute IOB of R_RST, R_CE : signal is \u0026#34;false\u0026#34;; signal R_DataIn : std_logic_vector(G_Width-1 downto 0); signal R_ValidIn : std_logic; attribute keep of R_DataIn, R_ValidIn : signal is \u0026#34;true\u0026#34;; attribute IOB of R_DataIn, R_ValidIn : signal is \u0026#34;false\u0026#34;; signal R_DataOut : std_logic_vector(G_Width-1 downto 0); signal R_ValidOut : std_logic; signal R_ReadyIn : std_logic; attribute keep of R_DataOut, R_ValidOut, R_ReadyIn : signal is \u0026#34;true\u0026#34;; attribute IOB of R_DataOut, R_ValidOut, R_ReadyIn : signal is \u0026#34;false\u0026#34;; --------------------------------------------------------------------------- -- Chaining arrays (sentinel element @0 and @G_PipelineModules) --------------------------------------------------------------------------- type T_DataArray is array(0 to G_PipelineModules) of std_logic_vector(G_Width-1 downto 0); signal S_Data : T_DataArray; signal S_Valid : std_logic_vector(0 to G_PipelineModules); signal S_Ready : std_logic_vector(0 to G_PipelineModules); begin GEN_Enable_CE : if G_Enable_CE = true generate process(I_CLK) begin if rising_edge(I_CLK) then R_CE \u0026lt;= I_CE; end if; end process; end generate GEN_Enable_CE; GEN_Enable_RST : if G_Enable_RST = true generate process(I_CLK) begin if rising_edge(I_CLK) then R_RST \u0026lt;= I_RST; end if; end process; end generate GEN_Enable_RST; ----------------------------------------------------------------------- -- Wrapper FFs: register all top‑level ports once for fair timing ----------------------------------------------------------------------- BenchFF : process(I_CLK) begin if rising_edge(I_CLK) then --- Register inputs R_DataIn \u0026lt;= I_Data; R_ValidIn \u0026lt;= I_Valid; O_Ready \u0026lt;= S_Ready(0); --- Register outputs R_DataOut \u0026lt;= S_Data (G_PipelineModules); R_ValidOut \u0026lt;= S_Valid(G_PipelineModules); R_ReadyIn \u0026lt;= I_Ready; end if; end process; O_Data \u0026lt;= R_DataOut; O_Valid \u0026lt;= R_ValidOut; ----------------------------------------------------------------------- -- Bind sentinel 0 with registered inputs ----------------------------------------------------------------------- S_Data (0) \u0026lt;= R_DataIn; S_Valid(0) \u0026lt;= R_ValidIn; ----------------------------------------------------------------------- -- Bind last sentinel with registered outputs ----------------------------------------------------------------------- S_Ready(G_PipelineModules) \u0026lt;= R_ReadyIn; ----------------------------------------------------------------------- -- Generate N pipeline modules in series ----------------------------------------------------------------------- gen_modules : for i in 0 to G_PipelineModules-1 generate P_MOD : entity work.Pipeline_pb_Module generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_Width =\u0026gt; G_Width, G_RegisterBalancing =\u0026gt; G_RegisterBalancing, G_EnablePipelineBuffer =\u0026gt; G_EnablePipelineBuffer ) port map( I_CLK =\u0026gt; I_CLK, I_RST =\u0026gt; R_RST, I_CE =\u0026gt; R_CE, -- Up‑stream side I_Data =\u0026gt; S_Data (i), I_Valid =\u0026gt; S_Valid(i), O_Ready =\u0026gt; S_Ready(i), -- Down‑stream side O_Data =\u0026gt; S_Data (i+1), O_Valid =\u0026gt; S_Valid(i+1), I_Ready =\u0026gt; S_Ready(i+1) ); end generate gen_modules; end architecture RTL; Ergebnisse Durch die Auftrennung des Ready-Signals mittels Skidbuffer entfällt dessen Propagierung durch die gesamte Pipeline. Valid ist dabei sowieso vollkommen durch Register entkoppelt. Dadurch wird jedes Modul hinsichtlich des Timings unabhängig von den anderen Modulen. Unabhängig davon, wie viele Module in der Pipeline sind, bleibt die erreichbare Taktfrequenz konstant bei 270 MHz.\nDas ganze ist auch in der hinsicht Testbar, dass ich den Skidbuffer in der AXI-Pipeline dynamisch aktivieren und deaktivieren kann. Dadurch kann ich die Auswirkungen des Skidbuffers auf die erreichbare Taktfrequenz messen. Ohne diesen sind bei 250 Modulen nicht mal mehr 100 MHz möglich.\n","date":"2025-07-18T19:46:03Z","image":"https://0xMax42.io/p/skidbuffer-in-acton/cover_hu_c95058437736d564.webp","permalink":"https://0xMax42.io/p/skidbuffer-in-acton/","title":"Skidbuffer in Acton"},{"content":"Mit v0.2.0 hat mein Deno‑basierter HTTP Kernel einen entscheidenden Schritt gemacht: Statt jede Middleware bei jedem Request dynamisch zu verkettet, wird die gesamte Kette jetzt einmalig kompiliert. Das reduziert Overhead, vereinfacht die Fehlerbehandlung – und macht das Routing spürbar schneller.\nAusgangslage Die erste Generation des Kernels setzte auf einen rekursiven dispatch()‑Mechanismus:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private async executePipeline( ctx: TContext, middleware: Middleware\u0026lt;TContext\u0026gt;[], handler: Handler\u0026lt;TContext\u0026gt;, ): Promise\u0026lt;Response\u0026gt; { const handleInternalError = (ctx: TContext, err?: unknown) =\u0026gt; this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR]( ctx, normalizeError(err), ); let lastIndex = -1; const dispatch = async (currentIndex: number): Promise\u0026lt;Response\u0026gt; =\u0026gt; { if (currentIndex \u0026lt;= lastIndex) { throw new Error(\u0026#39;Middleware called `next()` multiple times\u0026#39;); } lastIndex = currentIndex; const isWithinMiddleware = currentIndex \u0026lt; middleware.length; const fn = isWithinMiddleware ? middleware[currentIndex] : handler; if (isWithinMiddleware) { if (!isMiddleware(fn)) { throw new Error(\u0026#39;Expected middleware function, but received invalid value\u0026#39;); } return await fn(ctx, () =\u0026gt; dispatch(currentIndex + 1)); } if (!isHandler(fn)) { throw new Error(\u0026#39;Expected request handler, but received invalid value\u0026#39;); } return await fn(ctx); }; try { const response = await dispatch(0); return this.cfg.decorateResponse(response, ctx); } catch (e) { return handleInternalError(ctx, e); } } Dieser Ansatz war leicht zu verstehen, erzeugte aber pro Request mehrere Closures, Index‑Prüfungen und Typ‑Checks – alles Dinge, die bei hoher Parallelität unnötig Zeit kosten.\nDer neue Ansatz Beim Registrieren einer Route wird nun eine statisch verlinkte Funktionskette erzeugt. Ergebnis ist eine einzige Funktion runRoute(ctx), die alle Middlewares und den Handler inline ausführt.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private compile( route: Omit\u0026lt;IInternalRoute\u0026lt;TContext\u0026gt;, \u0026#39;runRoute\u0026#39; | \u0026#39;matcher\u0026#39; | \u0026#39;method\u0026#39;\u0026gt;, ): (ctx: TContext) =\u0026gt; Promise\u0026lt;Response\u0026gt; { if (!isHandler\u0026lt;TContext\u0026gt;(route.handler)) { throw new TypeError(\u0026#39;Route handler must be a function returning a Promise\u0026lt;Response\u0026gt;.\u0026#39;); } let composed = route.handler; for (let i = route.middlewares.length - 1; i \u0026gt;= 0; i--) { if (!isMiddleware\u0026lt;TContext\u0026gt;(route.middlewares[i])) { throw new TypeError(`Middleware at index ${i} is not a valid function.`); } const current = route.middlewares[i]; const next = composed; composed = async (ctx: TContext): Promise\u0026lt;Response\u0026gt; =\u0026gt; { let called = false; return await current(ctx, async () =\u0026gt; { if (called) { throw new Error(`next() called multiple times in middleware at index ${i}`); } called = true; return await next(ctx); }); }; } return composed; } Wichtig: Jede Middleware bekommt weiterhin ihr next() – jedoch kann dieses dank called‑Flag nur noch einmal aufgerufen werden, was Mehrfach‑Ausführungen zuverlässig verhindert.\nBenchmarks Gemessen mit 10 000 parallelen Requests auf einem Ryzen 7 8845HS:\nBenchmark (parallel) v0.1.0 (dynamisch) v0.2.0 (vorkompiliert) Gewinn Simple Route 5.4 ms 2.1 ms ≈ 61 % Complex Route 11.2 ms 7.9 ms ≈ 29 % Bei Einzeln‑Requests liegen die Werte nahezu gleichauf – der Geschwindigkeitsvorteil zeigt sich vor allem unter hoher Last.\nFazit Statisches Routing reduziert Overhead und steigert Durchsatz signifikant. next()‑Fehler werden jetzt sofort erkannt. API blieb kompatibel – Upgrade von 0.1.x auf 0.2.0 ist ohne Code‑Änderungen möglich. Der Kernel läuft bereits performant und stabil in meinem build‑cache‑server – einem Deno‑basierten Build‑Cache für act; kompatiibel mit der Github Actions Cache API.\n👉 Quellcode \u0026amp; Release‑Notes: https://github.com/0xMax42/http-kernel\n","date":"2025-05-27T13:00:00Z","image":"https://0xMax42.io/p/http-kernel-v0.2.0-von-dynamischem-dispatch-zur-vorkompilierten-pipeline/http-kernel-v0.2.0-cover_hu_7ba9f7ef043ef58e.webp","permalink":"https://0xMax42.io/p/http-kernel-v0.2.0-von-dynamischem-dispatch-zur-vorkompilierten-pipeline/","title":"HTTP Kernel v0.2.0 – Von dynamischem Dispatch zur vorkompilierten Pipeline"},{"content":"Wer schon mal versucht hat, systemd-Timer von Hand zu schreiben, weiß: So richtig Spaß macht das nicht. Zwischen .service- und .timer-Dateien, Pfadangaben, und OnCalendar-Syntax kann man sich schnell vertun. Genau dafür habe ich mir ein kleines Tool gebaut: systemd-timer.\nWas macht das Ding? Ganz einfach: Du rufst ein CLI auf, gibst ein paar Parameter an, und das Tool schreibt dir die passenden .service- und .timer-Units – inklusive Logging, Abhängigkeiten und allem, was systemd eben braucht. Keine Vorlage kopieren, keine Tippfehler mehr, kein Nachdenken darüber, ob das jetzt WantedBy=timers.target heißen muss oder nicht.\nWarum nicht einfach cron? Klar, cron funktioniert. Aber systemd hat ein paar Vorteile:\nBesseres Logging (auch in Datei) Einfache Integration mit anderen Diensten (z. B. Netzwerkwarten) Funktioniert auch pro Benutzer (ohne root-Rechte) Alles bleibt in einer Sprache: systemd Das CLI hilft dabei, die Einstiegshürde zu senken und trotzdem \u0026ldquo;richtig\u0026rdquo; mit systemd zu arbeiten.\nWas kann systemd-timer? .service + .timer-Dateien erstellen --user-Timer (für Desktop- oder Container-Umgebungen) Logging per --logfile Standard-Optionen wie: --exec, --calendar, --after, --environment --description, --output, --dry-run CLI basiert auf Cliffy, getippt mit Deno Plattformunabhängige Installation per Shell-Skript Ein Beispiel, das du sofort nutzen kannst 1 2 3 4 5 6 systemd-timer create \\ --exec \u0026#34;/usr/local/bin/backup.sh\u0026#34; \\ --calendar \u0026#34;Mon..Fri 02:00\u0026#34; \\ --description \u0026#34;Backup Job\u0026#34; \\ --user \\ --logfile \u0026#34;/var/log/backup.log\u0026#34; Ergebnis:\n~/.config/systemd/user/backup.service ~/.config/systemd/user/backup.timer Aktivieren:\n1 2 systemctl --user daemon-reload systemctl --user enable --now backup.timer Installation: Ein Einzeiler 1 curl -fsSL https://git.0xmax42.io/maxp/systemd-timer/raw/branch/main/scripts/install.sh | sh Das Skript erkennt automatisch deine Architektur (amd64/arm64), lädt die passende Binary und prüft sie mit SHA256. Und ja, du kannst dir den Code vorher anschauen, bevor du ihn durch die Leitung jagst.\nEntwicklung \u0026amp; Test Das Tool ist komplett mit TypeScript/Deno geschrieben, ordentlich typisiert und testgetrieben. Alles Wichtige ist modular aufgebaut, damit man es auch als Bibliothek verwenden könnte.\nTests laufen einfach per:\n1 deno task test Fazit systemd-timer ist kein Monster-Tool mit 1000 Optionen – sondern genau das, was man sich wünscht, wenn man öfter mal einfache Tasks zeitgesteuert per systemd laufen lassen will, ohne sich jedes Mal die Finger wundzuschreiben. Es richtet sich an Leute, die lieber deklarativ als improvisiert arbeiten.\nQuellcode, Releases und alles Weitere findest du hier: 👉 git.0xmax42.io/maxp/systemd-timer\n","date":"2025-05-24T21:30:00Z","image":"https://0xMax42.io/p/systemd-timer-per-cli-erstellen-einfach-schnell-nervenschonend/systemd-timer-cover_hu_47c3e5ed4845fb39.webp","permalink":"https://0xMax42.io/p/systemd-timer-per-cli-erstellen-einfach-schnell-nervenschonend/","title":"Systemd-Timer per CLI erstellen – einfach, schnell, nervenschonend"},{"content":"Ich habe ein neues Open-Source-Projekt veröffentlicht: PyDEPP – eine Python-Bibliothek für die Kommunikation mit Digilent-FPGA-Boards über das DEPP-Protokoll. Ziel war es, eine moderne, objektorientierte Schnittstelle für den Zugriff auf Register, Datenströme und Debug-Funktionen zu schaffen, die direkt auf dem Adept SDK von Digilent aufbaut.\nWarum das Ganze?\nDigilent stellt mit dem DEPP-Protokoll eine leistungsfähige, aber recht sperrig zu bedienende Schnittstelle bereit. Wer schon einmal versucht hat, mit den C-APIs zu arbeiten, weiß: Das geht eleganter. PyDEPP abstrahiert diese Komplexität und bringt den Zugriff auf Register und Streams auf Python-Niveau – inklusive Kontextmanagement via with, flexibler Timeout-Steuerung und Debug-Dumps.\nHighlights:\nget_reg() / set_reg() – einfacher Registerzugriff put_stream() / get_stream() – performante Datenübertragung Anpassbare Timeouts Kontext-Manager für sauberes Ressourcen-Handling Register-Dumps zur Fehlersuche Die Bibliothek ist vollständig quelloffen und unter MIT-Lizenz verfügbar. Wer auf Python-basierte Hardwarekommunikation setzt, wird hier fündig.\nGetestet mit:\nDigilent Nexys2 FPGA-Board Python 3.11 unter Debian Shared Libraries: libdmgr.so, libdepp.so aus dem Adept SDK ➡️ Wer sich für Embedded-Entwicklung mit Python interessiert oder einfach die DEPP-Schnittstelle besser nutzen möchte, ist herzlich eingeladen, mitzuwirken.\n","date":"2025-04-24T09:16:09Z","image":"https://0xMax42.io/p/pydepp-python-trifft-fpga/cover_hu_5cd014ca2f7c0df0.webp","permalink":"https://0xMax42.io/p/pydepp-python-trifft-fpga/","title":"Pydepp: Python Trifft Fpga"},{"content":"Heute widme ich mich der Optimierung einer kleinen Pipeline, die ich für ein Projekt erstellt habe. Die Pipeline ist dafür verantwortlich, den Adress-Offset eines Sprites zu berechnen und zu prüfen, ob dieses in der aktuell angeforderten Zeile sichtbar ist.\nBisher setze ich darauf, dass das Register Rebalancing seine Arbeit erledigt. Dies funktioniert auch, doch verbleiben klarerweise noch einige Optimierungsmöglichkeiten.\nZum Einstieg erst mal den aktuellen Code:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.all; use work.SpriteRom.all; entity VerticalSpritePipeline is generic ( --@ Width of the Y position (Line) register G_Y_Width : integer := 10; --@ The height of the sprite in pixels G_Sprite_Height : integer := 16; --@ Width of the sprite offset (Line address) register G_Offset_Width : integer := 8; --@ The pipeline stages for the calculating pipeline (multiply by 2 for the the latency of the pipeline) G_PipelineStages : integer := 2 ); port ( --@ Clock signal; (**Rising edge** triggered) I_CLK : in std_logic := \u0026#39;0\u0026#39;; --@ Clock enable signal (**Active high**) I_CE : in std_logic := \u0026#39;1\u0026#39;; --@ @virtualbus VSpritePipeline-OP @dir In Vertical sprite pipeline operation interfacee --@ AXI like ready; (**Synchronous**, **Active high**) O_VSpritePipeline_OP_Ready : out std_logic := \u0026#39;0\u0026#39;; --@ AXI like valid; (**Synchronous**, **Active high**) I_VSpritePipeline_OP_Valid : in std_logic := \u0026#39;0\u0026#39;; --@ The line to check if the sprite is in the line visible. I_VSpritePipeline_OP_Y_Request : in std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ The sprite Y position to check if the sprite is in the line visible. I_VSpritePipeline_OP_Y_Sprite : in std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ @end --@ @virtualbus VSpritePipeline-Result @dir Out Vertical sprite pipeline result interface --@ AXI like ready; (**Synchronous**, **Active high**) I_VSpritePipeline_Ready : in std_logic := \u0026#39;0\u0026#39;; --@ AXI like valid; (**Synchronous**, **Active high**) O_VSpritePipeline_Valid : out std_logic := \u0026#39;0\u0026#39;; --@ Indicates if the sprite is visible in the line. O_VSpritePipeline_IsVisible : out std_logic := \u0026#39;0\u0026#39;; --@ The calculated offset address of the sprite. O_VSpritePipeline_Offset : out std_logic_vector(G_Offset_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;) --@ @end ); end entity VerticalSpritePipeline; architecture Rtl of VerticalSpritePipeline is --@ Line to check if the sprite is in the line visible signal R_Y_Request : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ The sprite Y position to check if the sprite is in the line visible signal R_Y_Sprite : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ Calculated visibility signal signal C_IsVisible : std_logic := \u0026#39;0\u0026#39;; --@ The calculated offset address of the sprite signal C_Offset : std_logic_vector(G_Offset_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ Pipeline enable signal signal S_CalculatingPipeline_Enable : std_logic := \u0026#39;0\u0026#39;; begin --@ Pipeline controller for the calculating pipeline I_CalculatingPipelineCtrl : entity work.PipelineController generic map( G_PipelineStages =\u0026gt; G_PipelineStages * 2 ) port map( I_CLK =\u0026gt; I_CLK, I_CE =\u0026gt; I_CE, O_Enable =\u0026gt; S_CalculatingPipeline_Enable, I_Valid =\u0026gt; I_VSpritePipeline_OP_Valid, O_Ready =\u0026gt; O_VSpritePipeline_OP_Ready, O_Valid =\u0026gt; O_VSpritePipeline_Valid, I_Ready =\u0026gt; I_VSpritePipeline_Ready ); --@ Input register for the Y position of the sprite I_Y_InputRegister : entity work.PipelineRegister generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_Width =\u0026gt; G_Y_Width, G_RegisterBalancing =\u0026gt; \u0026#34;forward\u0026#34; ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; S_CalculatingPipeline_Enable, I_Data =\u0026gt; I_VSpritePipeline_OP_Y_Sprite, O_Data =\u0026gt; R_Y_Sprite ); --@ Input register for the line to check if the sprite is in the line visible I_YToCheck_InputRegister : entity work.PipelineRegister generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_Width =\u0026gt; G_Y_Width, G_RegisterBalancing =\u0026gt; \u0026#34;forward\u0026#34; ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; S_CalculatingPipeline_Enable, I_Data =\u0026gt; I_VSpritePipeline_OP_Y_Request, O_Data =\u0026gt; R_Y_Request ); --@ Combinatory process to calculate the visibility and offset of the sprite. P_CalculateVisibility : process (R_Y_Sprite, R_Y_Request) variable V_Y_Sprite : unsigned(R_Y_Sprite\u0026#39;range); variable V_Y_Request : unsigned(R_Y_Request\u0026#39;range); variable V_SpriteYBottom : unsigned(R_Y_Sprite\u0026#39;range); variable V_OffsetLine : integer; variable V_Offset : unsigned(C_Offset\u0026#39;range); begin V_Y_Sprite := unsigned(R_Y_Sprite); V_Y_Request := unsigned(R_Y_Request); V_SpriteYBottom := V_Y_Sprite + to_unsigned(G_Sprite_Height - 1, R_Y_Sprite\u0026#39;length); if V_Y_Request \u0026gt;= V_Y_Sprite and V_Y_Request \u0026lt;= V_SpriteYBottom then C_IsVisible \u0026lt;= \u0026#39;1\u0026#39;; else C_IsVisible \u0026lt;= \u0026#39;0\u0026#39;; end if; V_OffsetLine := to_integer(V_Y_Request - V_Y_Sprite); -- pragma translate_off if V_OffsetLine \u0026lt; 0 or V_OffsetLine \u0026gt;= K_SPRITE_ROW_OFFSETS\u0026#39;length then V_OffsetLine := 0; end if; -- pragma translate_on V_Offset := to_unsigned(K_SPRITE_ROW_OFFSETS(V_OffsetLine), C_Offset\u0026#39;length); C_Offset \u0026lt;= std_logic_vector(V_Offset); end process; --@ Output register for the visibility of the sprite I_IsVisible_OutputRegister : entity work.PipelineRegister generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_Width =\u0026gt; 1, G_RegisterBalancing =\u0026gt; \u0026#34;backward\u0026#34; ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; S_CalculatingPipeline_Enable, I_Data(0) =\u0026gt; C_IsVisible, O_Data(0) =\u0026gt; O_VSpritePipeline_IsVisible ); --@ Output register for the offset of the sprite I_Offset_OutputRegister : entity work.PipelineRegister generic map( G_PipelineStages =\u0026gt; G_PipelineStages, G_Width =\u0026gt; G_Offset_Width, G_RegisterBalancing =\u0026gt; \u0026#34;backward\u0026#34; ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; S_CalculatingPipeline_Enable, I_Data =\u0026gt; C_Offset, O_Data =\u0026gt; O_VSpritePipeline_Offset ); end architecture; Wie man erkennt, wird das Aufteilen der Berechnung aktuell vollständig der Synthese überlassen. Nun geht es darum, eine performante Aufteilung der Berechnung zu finden.\nIn dem Berechnungsprozess finden wir zwei relevante Berechnungen und einen Vergleich:\nBerechnung der unteren Sprite-Grenze\n1 V_SpriteYBottom := V_SpriteY + to_unsigned(G_Sprite_Height - 1, R_Y_Sprite\u0026#39;length); Vergleich der Y-Position mit der Sprite-Grenze (Sichtbarkeitsprüfung)\n1 2 if V_YToCheck \u0026gt;= V_SpriteY and V_YToCheck \u0026lt;= V_SpriteYBottom then Berechnung des Address-Offsets\n1 V_OffsetLine := to_integer(V_YToCheck - V_SpriteY); Da die Operationen Nr. 2 und 3 auf das Ergebnis der ersten Berechnung angewiesen sind, werden wir diese als Erstes in einer getrennten Stufe durchführen.\nAls Vorarbeit dazu arbeiten wir unsere Register zuerst einmal um. Zum einen passen wir die Benennung an und zum anderen definieren wir die Anzahl der Stufen nicht mehr dynamisch, sondern statisch.\nHierzu passen wir den Pipeline-Controller an:\n1 2 3 4 5 6 7 8 9 10 11 12 13 INST_VSpritePipeline_Ctrl : entity work.PipelineController generic map( G_PipelineStages =\u0026gt; 3 -- TODO ) port map( I_CLK =\u0026gt; I_CLK, I_CE =\u0026gt; I_CE, O_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Valid =\u0026gt; I_VSpritePipeline_OP_Valid, O_Ready =\u0026gt; O_VSpritePipeline_OP_Ready, O_Valid =\u0026gt; O_VSpritePipeline_Valid, I_Ready =\u0026gt; I_VSpritePipeline_Ready ); Und die beiden Eingangsregister, welche auf eine Stufe verkleinert wurden:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 INST0_VSpritePipeline_Y_Sprite : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data =\u0026gt; I_VSpritePipeline_OP_Y_Sprite, O_Data =\u0026gt; R0_Y_Sprite ); INST0_VSpritePipeline_Y_Request : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data =\u0026gt; I_VSpritePipeline_OP_Y_Request, O_Data =\u0026gt; R0_Y_Request ); Wir führen ein neues kombinatorisches und ein registriertes Signal C_Y_Bottom_Sprite bzw. R_Y_Bottom_Sprite ein, auf welches die Berechnung Nr. 1 geleitet wird. Die Berechnung wird gleichzeitig als concurrent signal assignment durchgeführt. Dies hat den Vorteil, dass wir die Berechnung nicht in einen Prozess packen müssen.\n1 2 3 4 --@ Calculate the bottom Y position of the sprite C_Y_Bottom_Sprite \u0026lt;= std_logic_vector( unsigned(R_Y_Sprite) + to_unsigned(G_Sprite_Height - 1, G_Y_Width) ); Weiter benötigen wir ein neues Register für diese Berechnung:\n1 2 3 4 5 6 7 8 9 10 INST_VSpritePipeline_Y_Bottom_Sprite : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data =\u0026gt; C_Y_Bottom_Sprite, O_Data =\u0026gt; R_Y_Bottom_Sprite ) Weiterhin brauchen müssen wir die Grundlagen unserer Berechnung ebenfalls eine Stufe weiter mitnehmen. Hierfür erstellen wir uns weitere Signale und erneut zwei Register:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 INST1_VSpritePipeline_Y_Sprite : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data =\u0026gt; R0_Y_Sprite, O_Data =\u0026gt; R1_Y_Sprite ); INST1_VSpritePipeline_Y_Request : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data =\u0026gt; R0_Y_Request, O_Data =\u0026gt; R1_Y_Request ); In der nächsten Stufe haben wir nun die registrierten Signale R1_Y_Sprite, R1_Y_Request und R_Y_Bottom_Sprite zur Verfügung. Die Operationen 2 und 3 aus unserer Liste können parallel durchgeführt werden. Hierzu verwenden wir die schon vorhandenen Signale C_IsVisible und C_Offset:\n1 2 3 4 5 6 7 8 9 10 11 12 --@ Calculate the visibility of the sprite C_IsVisible \u0026lt;= \u0026#39;1\u0026#39; when ( (unsigned(R1_Y_Request) \u0026gt;= unsigned(R1_Y_Sprite)) and (unsigned(R1_Y_Request) \u0026lt;= unsigned(R_Y_Bottom_Sprite)) ) else \u0026#39;0\u0026#39;; --@ Calculate the offset address of the sprite C_Offset \u0026lt;= std_logic_vector( to_unsigned( K_SPRITE_ROW_OFFSETS(to_integer(unsigned(R1_Y_Request) - unsigned(R1_Y_Sprite))), C_Offset\u0026#39;length) ); Diese beiden Werte sind nun unsere Ergebnisse der Pipeline und müssen letztmalig im Ausgangsregister gespeichert werden. Hierzu verwenden wir die schon vorhandenen Register und passen sie nur leicht an:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 INST_IsVisible_OutputRegister : entity work.PipelineRegister generic map( G_Width =\u0026gt; 1 ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data(0) =\u0026gt; C_IsVisible, O_Data(0) =\u0026gt; O_VSpritePipeline_IsVisible ); INST_Offset_OutputRegister : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Offset_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipelineCtrl_Enable, I_Data =\u0026gt; C_Offset, O_Data =\u0026gt; O_VSpritePipeline_Offset ); Zum Schluss müssten wir nun noch G_PipelineStages auf 3 setzen. Vorausschauend habe ich diesen Wert oben aber schon gesetzt, sodass wir hier nichts mehr anpassen müssen und das ``\u0026ndash; TODO` entfernen können.\nDer Code sieht nun wie folgt aus:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.all; use work.SpriteRom.all; entity VerticalSpritePipeline is generic ( --@ Width of the Y position (Line) register G_Y_Width : integer := 10; --@ The height of the sprite in pixels G_Sprite_Height : integer := 16; --@ Width of the sprite offset (Line address) register G_Offset_Width : integer := 8; --@ The pipeline stages for the calculating pipeline (multiply by 2 for the the latency of the pipeline) G_PipelineStages : integer := 2 ); port ( --@ Clock signal; (**Rising edge** triggered) I_CLK : in std_logic := \u0026#39;0\u0026#39;; --@ Clock enable signal (**Active high**) I_CE : in std_logic := \u0026#39;1\u0026#39;; --@ @virtualbus VSpritePipeline-OP @dir In Vertical sprite pipeline operation interfacee --@ AXI like ready; (**Synchronous**, **Active high**) O_VSpritePipeline_OP_Ready : out std_logic := \u0026#39;0\u0026#39;; --@ AXI like valid; (**Synchronous**, **Active high**) I_VSpritePipeline_OP_Valid : in std_logic := \u0026#39;0\u0026#39;; --@ The line to check if the sprite is in the line visible. I_VSpritePipeline_OP_Y_Request : in std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ The sprite Y position to check if the sprite is in the line visible. I_VSpritePipeline_OP_Y_Sprite : in std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ @end --@ @virtualbus VSpritePipeline-Result @dir Out Vertical sprite pipeline result interface --@ AXI like ready; (**Synchronous**, **Active high**) I_VSpritePipeline_Ready : in std_logic := \u0026#39;0\u0026#39;; --@ AXI like valid; (**Synchronous**, **Active high**) O_VSpritePipeline_Valid : out std_logic := \u0026#39;0\u0026#39;; --@ Indicates if the sprite is visible in the line. O_VSpritePipeline_IsVisible : out std_logic := \u0026#39;0\u0026#39;; --@ The calculated offset address of the sprite. O_VSpritePipeline_Offset : out std_logic_vector(G_Offset_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;) --@ @end ); end entity VerticalSpritePipeline; architecture Rtl of VerticalSpritePipeline is --@ Line to check if the sprite is in the line visible signal R0_Y_Request : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); signal R1_Y_Request : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ The sprite Y position to check if the sprite is in the line visible signal R0_Y_Sprite : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); signal R1_Y_Sprite : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ The bottom Y position of the sprite signal C_Y_Bottom_Sprite : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); signal R_Y_Bottom_Sprite : std_logic_vector(G_Y_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ Calculated visibility signal signal C_IsVisible : std_logic := \u0026#39;0\u0026#39;; --@ The calculated offset address of the sprite signal C_Offset : std_logic_vector(G_Offset_Width - 1 downto 0) := (others =\u0026gt; \u0026#39;0\u0026#39;); --@ Pipeline enable signal signal O_VSpritePipeline_Ctrl_Enable : std_logic := \u0026#39;0\u0026#39;; begin INST_VSpritePipeline_Ctrl : entity work.PipelineController generic map( G_PipelineStages =\u0026gt; 3 ) port map( I_CLK =\u0026gt; I_CLK, I_CE =\u0026gt; I_CE, O_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Valid =\u0026gt; I_VSpritePipeline_OP_Valid, O_Ready =\u0026gt; O_VSpritePipeline_OP_Ready, O_Valid =\u0026gt; O_VSpritePipeline_Valid, I_Ready =\u0026gt; I_VSpritePipeline_Ready ); INST0_VSpritePipeline_Y_Sprite : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data =\u0026gt; I_VSpritePipeline_OP_Y_Sprite, O_Data =\u0026gt; R0_Y_Sprite ); INST0_VSpritePipeline_Y_Request : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data =\u0026gt; I_VSpritePipeline_OP_Y_Request, O_Data =\u0026gt; R0_Y_Request ); --@ Calculate the bottom Y position of the sprite C_Y_Bottom_Sprite \u0026lt;= std_logic_vector( unsigned(R0_Y_Sprite) + to_unsigned(G_Sprite_Height - 1, G_Y_Width) ); INST_VSpritePipeline_Y_Bottom_Sprite : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data =\u0026gt; C_Y_Bottom_Sprite, O_Data =\u0026gt; R_Y_Bottom_Sprite ); INST1_VSpritePipeline_Y_Sprite : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data =\u0026gt; R0_Y_Sprite, O_Data =\u0026gt; R1_Y_Sprite ); INST1_VSpritePipeline_Y_Request : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Y_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data =\u0026gt; R0_Y_Request, O_Data =\u0026gt; R1_Y_Request ); --@ Calculate the visibility of the sprite C_IsVisible \u0026lt;= \u0026#39;1\u0026#39; when ( (unsigned(R1_Y_Request) \u0026gt;= unsigned(R1_Y_Sprite)) and (unsigned(R1_Y_Request) \u0026lt;= unsigned(R_Y_Bottom_Sprite)) ) else \u0026#39;0\u0026#39;; --@ Calculate the offset address of the sprite C_Offset \u0026lt;= std_logic_vector( to_unsigned( K_SPRITE_ROW_OFFSETS(to_integer(unsigned(R1_Y_Request) - unsigned(R1_Y_Sprite))), C_Offset\u0026#39;length) ); INST_IsVisible_OutputRegister : entity work.PipelineRegister generic map( G_Width =\u0026gt; 1 ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data(0) =\u0026gt; C_IsVisible, O_Data(0) =\u0026gt; O_VSpritePipeline_IsVisible ); INST_Offset_OutputRegister : entity work.PipelineRegister generic map( G_Width =\u0026gt; G_Offset_Width ) port map( I_CLK =\u0026gt; I_CLK, I_Enable =\u0026gt; O_VSpritePipeline_Ctrl_Enable, I_Data =\u0026gt; C_Offset, O_Data =\u0026gt; O_VSpritePipeline_Offset ); end architecture; Im Vergleich mit der vorherigen Version haben wir nun eine vorgelagerte Berechnung (1. Stufe) und die Berechnung der Sichtbarkeit und des Offsets (2. Stufe) parallelisiert. Dies sollte uns eine kleine Verbesserung hinsichtlich der Performance bringen. In meinem gesamten Design wurde damit die Pipeline vom Flaschenhals zum hinsichtlich des Timings unbeachtlichen Teil.\nInnerhalb der Synthese (Prä Place\u0026amp;Route) haben wir eine erhebliche Verbesserung auf dem eingesetzte Spartan 3 erreicht:\nVorher: Minimum period: 5.739ns (Maximum Frequency: 174.246MHz)\nNachher: Minimum period: 4.857ns (Maximum Frequency: 205.888MHz)\nDaher haben wir die Pipeline beschleunigt und den Code vereinfacht.\n","date":"2025-04-22T09:32:40Z","image":"https://0xMax42.io/p/pipeline-optimieren/cover_hu_7691051397864291.webp","permalink":"https://0xMax42.io/p/pipeline-optimieren/","title":"Pipeline Optimieren"},{"content":"In weniger als einer Stunde entstanden:\nEin kompakter Skidbuffer, der nur das ready-Signal entkoppelt – ohne zusätzliche Latenz.\nHintergrund ist hier die Verwendung von AXI-like Handshaking und die Notwendigkeit, das ready-Signal zu entkoppeln, um die Datenverarbeitung zu optimieren. In den von mir genutzten Desisngs ist das ready-Signal oft der Flaschenhals, da es in der Regel durch mehrere Pipelines hindurch propagiert werden muss. Ein Skidbuffer kann hier helfen, die Latenz zu reduzieren und die Datenverarbeitung zu optimieren. Dagegen wird das valid-Signal nicht entkoppelt, da es durch jede Pipeline selbst entkoppelt wird.\nDas Design nutzt ein einfaches Prinzip:\nWenn ready = '1', wird direkt durchgeschaltet (MUX = 0) Wenn ready = '0', wird ein Zwischenspeicher aktiviert (MUX = 1) valid bleibt entweder durchgeschliffen oder stammt aus dem Puffer Das System erfüllt vollständiges AXI-like Handshaking\nund wurde bei zufällig verzögertem Up- und Downstream erfolgreich getestet.\nRessourcenbedarf (nach Synthese, Xilinx Spartan-3):\n1 Flipflop 4 LUTs 0 zusätzliche Latenz 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 --@ Set mux to buffered mode if data is available in the buffer. C_MUX \u0026lt;= R_IsBuffered; --@ Enable the buffer register if not buffered and chip enable is high. C_Enable \u0026lt;= I_CE and not R_IsBuffered; --@ Set the ready signal to high if not buffered. O_Ready \u0026lt;= not R_IsBuffered; --@ Set the valid signal to high if data is available in the buffer or if data is valid. O_Valid \u0026lt;= R_IsBuffered or I_Valid; process (I_CLK) begin if rising_edge(I_CLK) then if I_RST = G_ResetActiveAt then R_IsBuffered \u0026lt;= \u0026#39;0\u0026#39;; elsif I_CE = \u0026#39;1\u0026#39; then if R_IsBuffered = \u0026#39;0\u0026#39; and I_Valid = \u0026#39;1\u0026#39; then R_IsBuffered \u0026lt;= \u0026#39;1\u0026#39;; elsif I_Ready = \u0026#39;1\u0026#39; and (R_IsBuffered or I_Valid) = \u0026#39;1\u0026#39; then R_IsBuffered \u0026lt;= \u0026#39;0\u0026#39;; end if; end if; end if; end process; Die Architektur eignet sich besonders für tiefe Pipeline-Systeme mit Timing-Engpässen auf ready.\nKein Overhead – nur Datenfluss.\n","date":"2025-04-19T19:14:03Z","image":"https://0xMax42.io/p/minimaler-skidbuffer-mit-axi-like-handshaking/cover_hu_dc8e01e57f90c5f.webp","permalink":"https://0xMax42.io/p/minimaler-skidbuffer-mit-axi-like-handshaking/","title":"Minimaler Skidbuffer mit AXI-like Handshaking"},{"content":"Einführung Dieser Beitrag dokumentiert meinen eigenen Gitea-Pages-Stack – eine datenschutzfreundliche Alternative zu GitHub Pages. Mit GT-RUNNER, dem Codeberg Pages Server und Traefik als Reverse Proxy entsteht ein selbstverwalteter Hosting-Stack, der Domains, Zertifikate und Deployments vollständig automatisiert – ohne zentrale Abhängigkeiten und mit voller Kontrolle.\nArchitekturüberblick Die folgende Skizze zeigt die Struktur meines Stacks. Alle zentralen Komponenten – Gitea, Runner, Pages Server und Reverse Proxy – sind im internen Netzwerk miteinander verbunden. Die TLS-Zertifikatsvergabe erfolgt gestaffelt über Let\u0026rsquo;s Encrypt (intern Staging, extern Production):\nFunktionsweise im Detail Ganz ehrlich: Das Ganze ist im Kern ein ganz normaler Git-Server. Gitea läuft intern, der GT-Runner ist angebunden – läuft über „act“ mit einem recht großen Image, bringt aber alles mit, was GitHub Actions auch kann. Für meine Zwecke ist das kompatibel genug.\nDazu kommt der Codeberg Pages Server – ebenfalls intern. Der spricht mit Gitea über die API, genau wie es gedacht ist. Wichtig: Alles, was Gitea, der Runner und der Pages-Server machen, bleibt komplett im internen Netzwerk. Da kommt nichts nach draußen. Das war mir wichtig.\nJetzt zum Thema TLS: Der Pages-Server kann nicht ohne Zertifikate. Also bekommt er welche – ganz regulär über meinen DNS-Provider, via API und ACME-Protokoll. Aber eben nur Staging-Zertifikate von Let\u0026rsquo;s Encrypt. Warum? Weil das reicht. Es geht um internes TLS, nicht um Vertrauen von außen.\nDer Pages-Server kann dann sauber ausliefern – intern, per HTTPS, mit gültiger Struktur. Und mein Reverse Proxy (Traefik) kümmert sich um den Rest.\nTraefik ist so konfiguriert, dass ihm egal ist, ob das interne Zertifikat „echt“ ist. Er terminated TLS nach außen hin selbst, holt sich dafür eigene echte Zertifikate über DNS-01 und Let\u0026rsquo;s Encrypt Production, und mappt die Domains sauber weiter.\nDamit das überhaupt funktioniert, schreibe ich die Requests um. Traefik nimmt z. B. home.pagessub.0xmax42.io, extrahiert daraus, um welches Repository es geht, und gaukelt dem Pages-Server vor, dass die URL intern user.pagessub.0xmax42.io/repo/ wäre. Damit der Pages-Server richtig routet.\nNach außen hin sieht alles aus wie bei GitHub Pages – jede Subdomain bekommt ihr eigenes statisches Projekt. Nur eben: datenschutzfreundlich, selbst gehostet, ohne Vendor-Lock-in.\nAusblick Langfristig will ich es ermöglichen, dass Repositories selbst konfigurieren können, unter welcher Domain sie ausgeliefert werden – z. B. über eine .traffic-Datei. Über die Gitea-API könnte dann automatisch die passende Traefik-Konfiguration erzeugt und an Traefik übergeben werden. Das Ziel: ein vollständig automatisierter Webhosting-Stack.\nVerwendete Komponenten 🧃 Gitea – Selfhosted Git-Service 🌀 Traefik – Reverse Proxy mit ACME-Support 📦 Codeberg Pages Server – statischer Pages-Server 🔀 traefik-subdomain-path-rewrite-plugin – für dynamisches URL-Rewriting basierend auf Subdomains 🔐 Let’s Encrypt – kostenfreies TLS-Zertifikatsmanagement via ACME-Protokoll 🐳 Docker – Containerisierung aller Komponenten zur einfachen Verwaltung und Isolierung 📁 Dieses Setup ist Teil meines Infrastrukturunterbaus für 0xMax42.io. Weitere Beiträge folgen.\n","date":"2025-04-19T00:00:00Z","image":"https://0xMax42.io/p/gitea-pages-stack/cover_hu_6b67d33469f93b45.webp","permalink":"https://0xMax42.io/p/gitea-pages-stack/","title":"Eigener Gitea-Pages-Stack mit Wildcard-Subdomains und ACME – mein GitHub-Pages-Ersatz"},{"content":"Ich bin zurück – mit neuem Namen, neuer Klarheit und einem neuen digitalen Zuhause.\nAb sofort findest du mich hier als 0xMax42.\nDiese Seite ist mehr als ein Blog. Sie ist mein Raum für Ideen, Systeme, Gedanken – und manchmal auch Widerstand.\nAlles, was du hier siehst, stammt direkt von mir – unabhängig, transparent und ohne Umwege.\nWillkommen bei 0xMax42.\n","date":"2025-04-17T00:00:00Z","image":"https://0xMax42.io/p/ich-bin-zurueck/cover_hu_46c3152cd99b6df6.webp","permalink":"https://0xMax42.io/p/ich-bin-zurueck/","title":"Ich bin zurück!"}]