After wrapping up my drone telemetry project in Season 1, I wanted to explore something that had always fascinated me — the boundary between software and hardware. That’s how this new season of Build, Create & Learn – A Maker’s Journey began.

This time, I’m stepping into Embedded Linux and Kernel Driver Development — a field that feels both intimidating and incredibly rewarding. If Season 1 was about learning embedded programming through microcontrollers, this one is about going beneath that layer — understanding how Linux itself communicates with devices.



Why the Kernel?

I’ve always loved experimenting with microcontrollers — but over time, I found myself wondering how much of that low-level control is hidden away when you move to a full Linux system.

In microcontroller programming, you handle everything directly: registers, timing, memory. In Linux, there’s an entire abstraction layer — and I wanted to understand it. That’s where kernel development comes in. The kernel is the bridge between applications and hardware. It’s where device drivers live, where memory is managed, and where system calls are handled.

It’s the heart of the operating system — and learning to interact with it feels like unlocking a new layer of understanding.

Setting Up My Linux Workbench

Instead of writing firmware for microcontrollers, I’m now working on kernel modules — small, loadable pieces of C code that extend the Linux kernel.

For that, I set up a dedicated environment:

  • Ubuntu 25.04 running inside UTM (QEMU) on macOS
  • A comfortable terminal setup using Zsh + Oh-My-Zsh
  • VS Code for editing and building
  • Kernel headers and build-essential packages for compiling modules

💡 Tip: If you’re using macOS, UTM is a great lightweight way to virtualize Linux for development — fully open source and surprisingly smooth for kernel experiments.

Before writing any code, I spent some time customizing my terminal and workflow. It may sound minor, but having an environment that feels “yours” makes learning more enjoyable — and it’s one of those small rituals that sets the tone for the project.

Writing My First “Hello Kernel” Module

My first milestone was simple but meaningful: getting a “Hello Kernel” message to appear in the system log.

It required only a few lines of C code:

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Stefan Herndlbauer");
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("A simple HelloWorld module");

static int __init helloworld_init(void) {
    printk(KERN_INFO "Hello World!\n");
    return 0;
}

static void __exit helloworld_exit(void) {
    printk(KERN_INFO "Goodbye World!\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);
  • Defining module metadata with macros like MODULE_LICENSE and MODULE_AUTHOR
  • Implementing two key functions: __init() and __exit()
  • Compiling with a simple Makefile
  • Loading it using insmod and checking the kernel log with dmesg

When I finally saw my message printed by the kernel, it felt different than any “Hello World” before — this time, the code was running inside the operating system itself.

It’s a strange but exciting feeling: your code is now part of the OS, not just running on top of it.

What Happens Before Linux Even Starts?

While learning, I realized how much happens before the kernel even takes control.

There’s a fascinating chain of steps that most of us never think about:

  1. ROM Bootloader – tiny code burned into the processor
  2. First-stage bootloader – initializes RAM and storage
  3. Second-stage bootloader (e.g., U-Boot) – loads the kernel image
  4. Kernel initialization – mounts file systems, starts processes

Understanding this sequence makes Linux feel less like a “black box” and more like a beautifully layered system — one that’s evolved over decades of engineering.

Why This Matters for Makers

For most maker projects, a microcontroller is enough. But as projects grow more complex — think robotics, drones, or computer vision — you need more computational power and multitasking ability. That’s where embedded Linux steps in.

There’s now a whole ecosystem of single-board computers that can run Linux: Raspberry Pi, BeagleBone, Orange Pi, Rock Pi, Radxa… each with its strengths.

What they all share is the ability to directly interact with GPIOs, sensors, and custom hardware through drivers. And while you can use Python libraries to access pins, I wanted to understand the mechanics underneath — how those abstractions are built.

That’s what this season is really about:

Learning how the operating system itself communicates with the world outside the CPU.

Learning Goals for This Season

This project is more than a technical challenge — it’s also a personal learning journey.

I want to:

  • Deepen my understanding of C programming
  • Learn how to build, debug, and load real kernel drivers
  • Explore cross-compiling for embedded targets like the Raspberry Pi
  • And most importantly — share every learning step openly, including the struggles and dead ends

It’s not about becoming a kernel developer overnight — it’s about exploring a new layer of the maker world, one line of C at a time.

What’s Next

In the next episode, I’ll move from the VM to real hardware — cross-compiling my first kernel module for Raspberry Pi.
That means dealing with ARM toolchains, kernel headers, and remote deployment.

If you’d like to follow along, I’ll share commands, configs, and troubleshooting notes in upcoming blog posts — and as always, everything will be documented here and in my newsletter.

📬 Subscribe to The Maker’s Logbook
Get weekly behind-the-scenes updates, progress notes, and extra resources:
👉 herndlbauer.com/pages/newsletter

🎧 Listen to the full episode:



Let’s keep building, creating, and learning — together.