<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: l5y</title>
    <description>The latest articles on DEV Community by l5y (@l5y).</description>
    <link>https://dev.to/l5y</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3811722%2F81b29bf0-8080-42cc-8a7c-6e3184240583.png</url>
      <title>DEV Community: l5y</title>
      <link>https://dev.to/l5y</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/l5y"/>
    <language>en</language>
    <item>
      <title>ArduLinux: running Arduino firmware on Linux</title>
      <dc:creator>l5y</dc:creator>
      <pubDate>Mon, 01 Jun 2026 21:32:56 +0000</pubDate>
      <link>https://dev.to/l5y/ardulinux-running-arduino-firmware-on-linux-2ong</link>
      <guid>https://dev.to/l5y/ardulinux-running-arduino-firmware-on-linux-2ong</guid>
      <description>&lt;p&gt;&lt;em&gt;I'm writing this from a mesh repeater that has no microcontroller in it. It's a Linux box running &lt;a href="https://meshcore.co.uk/" rel="noopener noreferrer"&gt;MeshCore&lt;/a&gt; firmware &lt;em&gt;natively&lt;/em&gt;, the same C++ that would normally live on an nRF52 or ESP32, compiled and running as an ordinary Linux process, talking to a real SPI radio. The thing that makes that possible is a small library called &lt;strong&gt;&lt;a href="https://github.com/l5yth/ardulinux" rel="noopener noreferrer"&gt;ArduLinux&lt;/a&gt;&lt;/strong&gt;, and this post is the story of why it exists.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: the Arduino API, as a Linux library
&lt;/h2&gt;

&lt;p&gt;Embedded firmware is written against the Arduino API, &lt;code&gt;digitalWrite()&lt;/code&gt;, &lt;code&gt;SPI.transfer()&lt;/code&gt;, &lt;code&gt;Serial.printf()&lt;/code&gt;, &lt;code&gt;Wire.begin()&lt;/code&gt;, and friends. ArduLinux implements that API as a Linux user-space library, so firmware written for microcontrollers builds and runs on a PC or a Raspberry Pi &lt;strong&gt;unmodified&lt;/strong&gt;. It wires the Arduino calls to real hardware, GPIO via libgpiod, SPI via spidev, I2C via i2c-dev, Serial via POSIX file descriptors, or to fully simulated devices when you just want to run in CI.&lt;/p&gt;

&lt;p&gt;Using it is one block of &lt;code&gt;platformio.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[env:ardulinux]&lt;/span&gt;
&lt;span class="py"&gt;platform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;git+https://github.com/l5yth/ardulinux.git&lt;/span&gt;
&lt;span class="py"&gt;framework&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;arduino&lt;/span&gt;
&lt;span class="py"&gt;board&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ardulinux&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your &lt;code&gt;setup()&lt;/code&gt; and &lt;code&gt;loop()&lt;/code&gt; run on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing on portduino's shoulders
&lt;/h2&gt;

&lt;p&gt;None of this is a new idea. ArduLinux is a &lt;strong&gt;clean-room continuation of &lt;a href="https://github.com/geeksville/framework-portduino" rel="noopener noreferrer"&gt;portduino&lt;/a&gt;&lt;/strong&gt;, the project (from the Meshtastic world, Kevin Hester, Jonathan Bennett, and many others) that pioneered running Arduino firmware on Linux. Portduino did the genuinely hard part: proving the approach works and carrying it for years. I want to be clear up front, this is a continuation written in gratitude, not a complaint.&lt;/p&gt;

&lt;p&gt;But when I went to use portduino as a &lt;em&gt;dependency&lt;/em&gt; for a fresh project, I kept tripping over the same thing: it was held together with patches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monkey-patch problem
&lt;/h2&gt;

&lt;p&gt;Here's the concrete example that started it. The upstream &lt;a href="https://github.com/arduino/ArduinoCore-API" rel="noopener noreferrer"&gt;ArduinoCore-API&lt;/a&gt; &lt;code&gt;Print&lt;/code&gt; class gives you &lt;code&gt;print()&lt;/code&gt; and &lt;code&gt;println()&lt;/code&gt;, but &lt;strong&gt;no &lt;code&gt;printf()&lt;/code&gt;&lt;/strong&gt;. Tons of firmware (MeshCore included) calls &lt;code&gt;Serial.printf(...)&lt;/code&gt;. Upstream hasn't added it. So you have to shim it in somewhere.&lt;/p&gt;

&lt;p&gt;The way that shim had grown was, roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Copy the ENTIRE upstream API tree, minus Print.h...
&lt;/span&gt;&lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copytree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upstream_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patched_api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;ignore_patterns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Print.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# ...then drop in our own hand-edited Print.h that adds printf().
&lt;/span&gt;&lt;span class="nf"&gt;write_patched_print_h&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patched_api/Print.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at what that actually does: to add one method, you make a &lt;strong&gt;full, divergent copy of the upstream library&lt;/strong&gt; at build time. And it wasn't only &lt;code&gt;Print.h&lt;/code&gt;, the dependencies themselves (ArduinoCore-API, the WiFi library) were vendored as copied-and-patched source trees rather than tracked upstream.&lt;/p&gt;

&lt;p&gt;That pattern has three nasty consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You can't follow upstream.&lt;/strong&gt; The pinned ArduinoCore-API had fallen ~180 commits behind, and bumping it meant re-doing the patches by hand. So nobody bumped it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The copy silently goes stale.&lt;/strong&gt; The cached copy keyed on a &lt;em&gt;path&lt;/em&gt;, not a version, so after a bump it could happily keep serving the old copy, and you'd never know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs hide in the machinery.&lt;/strong&gt; When the build is this baroque, latent bugs go unnoticed. (More on the two I found below.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The general principle, and it applies to &lt;em&gt;every&lt;/em&gt; monkey-patched library, not just this one: &lt;strong&gt;the moment you copy-and-edit a dependency, you've silently forked it&lt;/strong&gt;, and given up the one thing a dependency is supposed to give you, which is the ability to pull upstream's fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just fix it inside portduino?
&lt;/h2&gt;

&lt;p&gt;Fair question, and I tried to convince myself it was the right move. It wasn't, for two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's structural, not cosmetic.&lt;/strong&gt; The vendoring and the copytree shim weren't a bad function you could rewrite over a weekend; they were baked into the architecture, the package layout, the builder, the dependency model. Undoing them properly means re-laying the foundation. That's not a patch, it's a different building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;portduino is load-bearing for other people.&lt;/strong&gt; It's a live dependency of Meshtastic. A sweeping "rip out the vendoring and rewrite the build" change against a tree that thousands of devices depend on is exactly the kind of disruptive churn you &lt;em&gt;shouldn't&lt;/em&gt; push onto a working project to suit your own narrower goals.&lt;/p&gt;

&lt;p&gt;So I did the open-source thing: &lt;strong&gt;fork, clean up, and present.&lt;/strong&gt; A continuation lets you re-establish the invariant, &lt;em&gt;depend on upstream, unmodified&lt;/em&gt;, from a clean slate, without destabilizing anyone who's happy with what they have. If it's useful to others, wonderful. If not, portduino is right where it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in ArduLinux
&lt;/h2&gt;

&lt;p&gt;The whole renovation is in service of one rule: &lt;strong&gt;clean upstream dependencies, and isolate the single unavoidable shim.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies are real git submodules&lt;/strong&gt;, tracked at their upstream commits, not copied, not patched. Bumping ArduinoCore-API from 1.1.0 to 1.5.2 became a one-line gitlink change instead of a manual re-patching session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;printf&lt;/code&gt; shim is now a symlink overlay.&lt;/strong&gt; Instead of copying the whole API and stripping a file, every upstream header is symlinked as-is, and we shadow &lt;em&gt;exactly one&lt;/em&gt; file, &lt;code&gt;Print.h&lt;/code&gt;, re-synced verbatim from upstream with the &lt;code&gt;printf()&lt;/code&gt; method added. No copy, nothing to drift.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Symlink every upstream header as-is, the real files, never copied...
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upstream_headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlay&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...and shadow ONLY Print.h: the one minimal, honest shim.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead code and clutter removed&lt;/strong&gt;, out-of-line duplicates, unused board variants, IDE project files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the payoff that justifies the whole exercise: with the build no longer hiding things, two real bugs surfaced and got fixed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/meshtastic/framework-portduino/issues/71" rel="noopener noreferrer"&gt;#71&lt;/a&gt; An I2C handle defaulted to file descriptor &lt;code&gt;0&lt;/code&gt;, &lt;strong&gt;stdin&lt;/strong&gt;, so a read before &lt;code&gt;begin()&lt;/code&gt; would silently block on the terminal forever. (It now defaults to &lt;code&gt;-1&lt;/code&gt; and fails fast, like the serial port already did.)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/meshtastic/framework-portduino/issues/72" rel="noopener noreferrer"&gt;#72&lt;/a&gt; The &lt;code&gt;printf&lt;/code&gt; shim trusted &lt;code&gt;vsnprintf&lt;/code&gt;'s return value, which is the length it &lt;em&gt;would&lt;/em&gt; have written, a classic &lt;strong&gt;buffer over-read&lt;/strong&gt; on long strings. (Now clamped; verified under AddressSanitizer.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither was introduced by the cleanup; both were lurking. Simpler machinery just made them visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Room to add new things
&lt;/h2&gt;

&lt;p&gt;Tidying the dependency story is only half of why the fork was worth it. The other half: once you own the runtime outright, you can make it behave like a proper Linux program.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binary customization.&lt;/strong&gt; An app sets its own name, version, description, and bug-report address through a few optional weak symbols, no header required. It then introduces itself in its startup banner and &lt;code&gt;--version&lt;/code&gt;, names its data directory after itself, and labels its libgpiod lines with it. &lt;code&gt;meshcored&lt;/code&gt; shows up as &lt;code&gt;meshcored&lt;/code&gt;, not a generic &lt;code&gt;program&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XDG-compliant data directories.&lt;/strong&gt; The virtual filesystem lives where a well-behaved Linux app keeps state, &lt;code&gt;$XDG_DATA_HOME/&amp;lt;app&amp;gt;/default&lt;/code&gt; (so &lt;code&gt;~/.local/share/meshcored/...&lt;/code&gt;), instead of scattering dotfiles across &lt;code&gt;$HOME&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real command line.&lt;/strong&gt; &lt;code&gt;--fsdir&lt;/code&gt; to relocate that state, &lt;code&gt;--erase&lt;/code&gt; to wipe it on startup, plus the usual &lt;code&gt;--version&lt;/code&gt;, &lt;code&gt;--help&lt;/code&gt;, and &lt;code&gt;--usage&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the list keeps growing, because now there's somewhere natural to put it. So I'd gently resist the idea that this was only about chasing upstream versions: tracking upstream cleanly is the discipline that made the rest possible, but the visible, day-to-day payoff is a binary that starts up, stores data, and takes its flags like every other tool on the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof
&lt;/h2&gt;

&lt;p&gt;The real test wasn't the unit suite, it was whether a downstream project could actually consume this. So I pointed my MeshCore-Linux branch at the first release tag &lt;code&gt;v0.2.0&lt;/code&gt; as a pinned dependency and rebuilt.&lt;/p&gt;

&lt;p&gt;It worked &lt;strong&gt;out of the box.&lt;/strong&gt; I'm running a Linux-native MeshCore repeater on it right now, no complaints. That's the moment the whole "clean dependencies" thesis stopped being a principle and became a working radio on my desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;ArduLinux is on the &lt;a href="https://registry.platformio.org/" rel="noopener noreferrer"&gt;PlatformIO registry&lt;/a&gt; and &lt;a href="https://github.com/l5yth/ardulinux" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, LGPL-2.1, built on the work of the Arduino project, the portduino authors, and everyone who came before this split. If you've got embedded C++ you'd like to run, test, or debug on a real Linux machine, with &lt;code&gt;gdb&lt;/code&gt;, with sanitizers, in CI, on actual GPIO, give it a spin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--recurse-submodules&lt;/span&gt; https://github.com/l5yth/ardulinux.git
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build
./build/ArduLinux &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the spirit of open source: you fork, you clean up, you present, and you see if anyone else finds it useful. Maybe you will.&lt;/p&gt;

</description>
      <category>arduino</category>
      <category>linux</category>
      <category>cpp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Enabling SPI on Arch Linux ARM (Raspberry Pi 4, aarch64)</title>
      <dc:creator>l5y</dc:creator>
      <pubDate>Mon, 01 Jun 2026 16:06:59 +0000</pubDate>
      <link>https://dev.to/l5y/enabling-spi-on-arch-linux-arm-raspberry-pi-4-aarch64-ci4</link>
      <guid>https://dev.to/l5y/enabling-spi-on-arch-linux-arm-raspberry-pi-4-aarch64-ci4</guid>
      <description>&lt;p&gt;&lt;em&gt;You add &lt;code&gt;dtparam=spi=on&lt;/code&gt; to &lt;code&gt;/boot/config.txt&lt;/code&gt;, reboot, and... there's no &lt;code&gt;/dev/spidev*&lt;/code&gt;. On the &lt;strong&gt;AArch64&lt;/strong&gt; Arch Linux ARM image this is expected, and the fix isn't more &lt;code&gt;config.txt&lt;/code&gt; tweaking. Here's the clean way.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The AArch64 ALARM image boots the &lt;strong&gt;mainline kernel via U-Boot&lt;/strong&gt;, which ignores &lt;code&gt;config.txt&lt;/code&gt;. Switch to the &lt;strong&gt;Foundation kernel (&lt;code&gt;linux-rpi&lt;/code&gt;)&lt;/strong&gt; and SPI + overlays work natively. One board-specific trap to watch: the shipped &lt;code&gt;cmdline.txt&lt;/code&gt; has the wrong &lt;code&gt;root=&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Tested on: Raspberry Pi 4B, Arch Linux ARM aarch64, &lt;code&gt;/boot&lt;/code&gt; on a separate FAT partition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;dtparam=spi=on&lt;/code&gt; does nothing
&lt;/h2&gt;

&lt;p&gt;The GPU firmware reads &lt;code&gt;config.txt&lt;/code&gt; and patches &lt;em&gt;its own&lt;/em&gt; device tree, but U-Boot then loads a &lt;strong&gt;different&lt;/strong&gt; mainline DTB from &lt;code&gt;/boot/dtbs&lt;/code&gt; and boots that, throwing the firmware's edit away. Forcing the firmware DTB doesn't help either: the mainline &lt;code&gt;spidev&lt;/code&gt; driver won't bind to its &lt;code&gt;compatible = "spidev"&lt;/code&gt; nodes.&lt;/p&gt;

&lt;p&gt;The Foundation kernel sidesteps all of it: the firmware boots it &lt;strong&gt;directly&lt;/strong&gt; and applies &lt;code&gt;config.txt&lt;/code&gt; + overlays the way every Pi guide assumes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This swaps your kernel and removes U-Boot. Keep a serial/HDMI console or SD-card access handy in case of a bad boot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  0. Prep
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Note your real root partition, you'll need this exact value in step 2.&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/cmdline                       &lt;span class="c"&gt;# grab the root=PARTUUID=... part&lt;/span&gt;
lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; NAME,PARTUUID,MOUNTPOINT

&lt;span class="c"&gt;# Back up /boot and cache the current packages for rollback.&lt;/span&gt;
&lt;span class="nb"&gt;sudo tar &lt;/span&gt;czf /root/boot-backup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /boot &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-Sw&lt;/span&gt; linux-aarch64 uboot-raspberrypi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Install the Foundation kernel
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; linux-rpi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It &lt;strong&gt;conflicts with &lt;code&gt;uboot-raspberrypi&lt;/code&gt; and &lt;code&gt;linux-aarch64&lt;/code&gt;&lt;/strong&gt; and removes both, that's expected. It drops in the Foundation kernel as &lt;code&gt;/boot/kernel8.img&lt;/code&gt;, ~360 overlays, and fresh &lt;code&gt;config.txt&lt;/code&gt; / &lt;code&gt;cmdline.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fix &lt;code&gt;cmdline.txt&lt;/code&gt;, the one guaranteed gotcha
&lt;/h2&gt;

&lt;p&gt;The shipped &lt;code&gt;/boot/cmdline.txt&lt;/code&gt; hardcodes &lt;code&gt;root=/dev/mmcblk0p2&lt;/code&gt;. If your card enumerates differently (mine is &lt;code&gt;mmcblk1&lt;/code&gt;), that's an instant no-boot. Replace the &lt;code&gt;root=&lt;/code&gt; with &lt;strong&gt;your&lt;/strong&gt; value from step 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /boot/cmdline.txt  (single line)
&lt;/span&gt;&lt;span class="py"&gt;root&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PARTUUID=xxxxxxxx-02 rw rootwait fsck.repair=yes console=serial0,115200 console=tty1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;blkid&lt;/code&gt; to determine UUID.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Re-enable SPI in &lt;code&gt;config.txt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;config.txt&lt;/code&gt; ships with SPI commented out. Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /boot/config.txt
&lt;/span&gt;&lt;span class="py"&gt;enable_uart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;dtparam&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;spi=on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Sanity-check before rebooting
&lt;/h2&gt;

&lt;p&gt;Can't hurt to run &lt;code&gt;mkinitcpio -p linux-rpi&lt;/code&gt; again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /boot/kernel8.img            &lt;span class="c"&gt;# multi-MB = Foundation kernel (U-Boot was ~700 KB)&lt;/span&gt;
&lt;span class="nb"&gt;grep &lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; /boot/cmdline.txt       &lt;span class="c"&gt;# YOUR PARTUUID, not mmcblk0p2&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'spi|initramfs'&lt;/span&gt; /boot/config.txt
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /boot/initramfs-linux.img    &lt;span class="c"&gt;# freshly rebuilt&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /lib/modules/                   &lt;span class="c"&gt;# e.g. 6.x-rpi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Reboot &amp;amp; verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;span class="c"&gt;# once it's back:&lt;/span&gt;
&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;                           &lt;span class="c"&gt;# ...-rpi&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /dev/spidev&lt;span class="k"&gt;*&lt;/span&gt;                 &lt;span class="c"&gt;# spidev0.0  spidev0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Optional: use SPI without root
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/udev/rules.d/99-spi.rules
&lt;/span&gt;&lt;span class="py"&gt;SUBSYSTEM&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;="spidev", GROUP="spi", MODE="0660"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;groupadd &lt;span class="nt"&gt;-f&lt;/span&gt; spi &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; spi &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# re-login to apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick loopback test: jumper &lt;strong&gt;pin 19 ↔ pin 21&lt;/strong&gt; (MOSI↔MISO) and run &lt;code&gt;spidev_test&lt;/code&gt;, TX should equal RX.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPI0 pinout (40-pin header)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;BCM&lt;/th&gt;
&lt;th&gt;Pin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MOSI&lt;/td&gt;
&lt;td&gt;GPIO10&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MISO&lt;/td&gt;
&lt;td&gt;GPIO9&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCLK&lt;/td&gt;
&lt;td&gt;GPIO11&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CE0&lt;/td&gt;
&lt;td&gt;GPIO8&lt;/td&gt;
&lt;td&gt;24 → &lt;code&gt;spidev0.0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CE1&lt;/td&gt;
&lt;td&gt;GPIO7&lt;/td&gt;
&lt;td&gt;26 → &lt;code&gt;spidev0.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Rollback
&lt;/h2&gt;

&lt;p&gt;If it won't boot: pull the SD card, restore &lt;code&gt;/boot&lt;/code&gt; from &lt;code&gt;boot-backup.tar.gz&lt;/code&gt;, and reinstall &lt;code&gt;linux-aarch64&lt;/code&gt; + &lt;code&gt;uboot-raspberrypi&lt;/code&gt; from the pacman cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; now that the firmware honors &lt;code&gt;config.txt&lt;/code&gt;, other peripherals are one line away too, &lt;code&gt;dtparam=i2c_arm=on&lt;/code&gt; for I2C, or any overlay in &lt;code&gt;/boot/overlays&lt;/code&gt; via &lt;code&gt;dtoverlay=...&lt;/code&gt;. No device-tree surgery required.&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>archlinux</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>psn - a minimalist process navigator for Linux</title>
      <dc:creator>l5y</dc:creator>
      <pubDate>Sat, 07 Mar 2026 15:45:40 +0000</pubDate>
      <link>https://dev.to/l5y/psn-a-minimalist-process-navigator-for-linux-37jn</link>
      <guid>https://dev.to/l5y/psn-a-minimalist-process-navigator-for-linux-37jn</guid>
      <description>&lt;p&gt;When a process hangs up on me on a server, my usual workflow is a mix of &lt;code&gt;ps aux | grep&lt;/code&gt;, &lt;code&gt;pgrep&lt;/code&gt;, copying PIDs, and reaching for &lt;code&gt;kill&lt;/code&gt;. It works, but it interrupts my workflows. I decided to go for a little &lt;em&gt;quality-of-life&lt;/em&gt; upgrade and implement &lt;code&gt;psn&lt;/code&gt;, the &lt;em&gt;process status navigator&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/l5yth/psn" rel="noopener noreferrer"&gt;psn&lt;/a&gt; is a small Rust TUI. You launch it, find the process, and send it a signal; all without leaving the terminal or typing a PID by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://asciinema.org/a/820051" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faha17x84jfzoc5gjfp5g.png" alt="asciicast" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem it solves
&lt;/h2&gt;

&lt;p&gt;The command-line is where I spend most of my time and earn a living: personal laptops, remote servers, etc. The tooling story for process management is surprisingly fragile. I started to become a power user of &lt;code&gt;btop&lt;/code&gt;, my favorite &lt;em&gt;*top-derivate,&lt;/em&gt; but kept falling back to &lt;code&gt;ps&lt;/code&gt; when I had to deal with processes. However, its output is a wall of text that you have to mentally parse or pipe through &lt;code&gt;grep&lt;/code&gt; and then act on with a subsequent command.&lt;/p&gt;

&lt;p&gt;The real need is simple: find a process by name and send it a signal. It cannot be so hard? ;)&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;psn&lt;/code&gt; displays a table of running processes inside a navigatable terminal user interface. Each row of the table shows the process name, PID, user, CPU and memory usage, status, and the full command string. Processes are arranged in a parent–child tree so you can see what spawned what.&lt;/p&gt;

&lt;p&gt;From there you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate with arrow keys up/down and page up/down&lt;/li&gt;
&lt;li&gt;Collapse and expand subtrees with arrow keys left/right&lt;/li&gt;
&lt;li&gt;Filter on the fly by pressing &lt;code&gt;/&lt;/code&gt; and typing - the list narrows as you type, and matched text is highlighted in the name and command columns&lt;/li&gt;
&lt;li&gt;Pre-filter at launch by passing a substring (&lt;code&gt;psn cargo&lt;/code&gt;) or a regular expression (&lt;code&gt;psn -r '^rust'&lt;/code&gt;) as an argument&lt;/li&gt;
&lt;li&gt;Send a signal by pressing &lt;code&gt;1&lt;/code&gt; through &lt;code&gt;9&lt;/code&gt;; the key maps directly to the Unix signal number - &lt;code&gt;9&lt;/code&gt; is SIGKILL, &lt;code&gt;1&lt;/code&gt; is SIGHUP, &lt;code&gt;2&lt;/code&gt; is SIGINT, and so on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. I didn't ask for more and I also don't think it needs more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The fastest path is via &lt;a href="https://crates.io/crates/psn" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cargo install psn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Arch Linux the &lt;a href="https://aur.archlinux.org/packages/psn-bin" rel="noopener noreferrer"&gt;psn-bin&lt;/a&gt; AUR package installs a prebuilt binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yay -S psn-bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or build from the &lt;a href="https://github.com/l5yth/psn" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; directly with &lt;code&gt;cargo build --release&lt;/code&gt;. A &lt;a href="https://github.com/l5yth/psn/blob/main/flake.nix" rel="noopener noreferrer"&gt;Nix flake&lt;/a&gt; and a &lt;a href="https://github.com/l5yth/psn/tree/main/packaging/gentoo" rel="noopener noreferrer"&gt;Gentoo ebuild&lt;/a&gt; are also available.&lt;/p&gt;

&lt;p&gt;The only runtime dependency is a GNU/Linux system with &lt;code&gt;procfs&lt;/code&gt; support. No &lt;code&gt;ps&lt;/code&gt; wrapper, no external binaries - process data is read directly via &lt;a href="https://crates.io/crates/sysinfo" rel="noopener noreferrer"&gt;sysinfo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you spend any meaningful time on Linux servers and find yourself reaching for &lt;code&gt;ps | grep | kill&lt;/code&gt; more than you'd like, give &lt;code&gt;psn&lt;/code&gt; a try. Feedback and issues are welcome!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>linux</category>
      <category>tui</category>
      <category>pid</category>
    </item>
  </channel>
</rss>
