No description
  • C 72.6%
  • Nix 23.9%
  • Typst 1.8%
  • Makefile 1.7%
Find a file
2026-05-20 10:58:27 +02:00
docs Added example video 2026-04-13 18:26:06 +02:00
module Removed useless locks 2026-05-20 10:58:27 +02:00
morsesh removed outdated comment 2026-04-12 22:27:37 +02:00
questionnaire Alot of small changes 2026-04-12 22:21:29 +02:00
.clang-format Enabled AlignConsecutiveMacros for clang-format 2026-04-08 09:09:23 +02:00
.clangd unit-tests 2026-04-10 15:44:50 +02:00
.gitignore Alot of small changes 2026-04-12 22:21:29 +02:00
flake.lock Finally a solid flake, nix is a little tedious, but this shit will build in 2100 2026-04-04 23:55:34 +02:00
flake.nix Small mistakes in flake, added c ouput files to gitignore 2026-04-20 00:38:51 +02:00
LICENSE Added Jacob to license 2026-04-11 16:35:16 +02:00
README.md readme update 2026-04-13 20:09:16 +02:00

Morse Module

A Linux kernel module for Raspberry Pi Zero 2 that exposes a character device (/dev/morse_dev) accepting plain ASCII text and blinking it as Morse code on a

Example Video of The Driver


Table of Contents


Features

  • Character device at /dev/morse_dev, that blinks the GPIO pin in Morse code
  • Configurable GPIO pin and transmission speed (WPM) via ioctl
  • Kernel transmit thread with a buffered output queue
  • ioctl flush command to discard pending output instantly
  • HAL abstraction layer enabling unit tests to run on the host without hardware
  • Userspace shell (morsesh) for interactive use

Building

Cross-compiling With Nix

# Build everything (module, tests, morsesh, headers, questionnaire)
nix build
tree result/

The combined output looks like:

result/
├── bin/
│   ├── morsesh             # aarch64 interactive terminal
│   └── morse-test          # host unit test binary
├── docs/
│   └── questionnaire.pdf   # even questionnaire document
├── include/
│   └── morse/
│       └── morse_ioctl.h   # userspace header
└── lib/
    └── modules/
        └── morse.ko        # aarch64 kernel module

Individual targets are also available:

nix build .#morse-module   # kernel module only
nix build .#morse-tests    # unit test binary
nix build .#morsesh        # userspace shell
nix build .#morse-dev      # userspace headers

Local on The Raspberry Pi with Make

If you prefer to build directly on the Pi against the running kernel:

cd module
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

Unit Tests

The encoding logic is tested on the host using CUnit against a stub HAL (morse_hal_stub.c) that records pin transitions instead of touching GPIO hardware.

Run via Nix:

nix build .#morse-tests
./result/bin/morse-test

Or manually:

cd module
gcc -std=c11 -Wall -Wextra -g \
    -I. -I./include \
    $(pkg-config --cflags cunit) \
    tests/test_morse.c morse_tx.c morse_hal_stub.c \
    -o morse_tests \
    $(pkg-config --libs cunit)
./morse_tests

Usage

Loading the Module

Copy morse.ko to the Pi, then load it:

sudo insmod morse.ko

If /dev/morse_dev isn't present, create it manually:

sudo mknod /dev/morse_dev c 255 0
sudo chmod 666 /dev/morse_dev

To unload:

sudo rmmod morse

Kernel log output (pin, WPM, thread lifecycle) is available via:

dmesg | grep Morse

Sending Text

Write any ASCII string to the device and the LED will transmit it:

echo "Hello World" > /dev/morse_dev
echo "SOS" > /dev/morse_dev

Transmission is buffered; subsequent writes are queued behind any ongoing output.

Blocking behaviour

By default the device opens in blocking mode. If the internal buffer is full (because the transmitter is still blinking out earlier text), the write call will sleep until there is room - the calling process is not busy-waiting, it is put to sleep by the kernel and woken up when the TX thread consumes data.

To avoid blocking, open the device with O_NONBLOCK. In that case a full buffer causes write to return -EAGAIN immediately, so the caller can decide what to do rather than waiting. If some bytes were already written before the buffer filled, the number of bytes written so far is returned instead of -EAGAIN.

ioctl Interface

The device supports three ioctl commands, defined in include/morse/morse_ioctl.h:

Command Number Argument Description
MORSE_SET_PIN 1 int pin Change the GPIO output pin
MORSE_SET_WPM 2 int wpm Set speed in WPM (130)
MORSE_FLUSH 3 (none) Discard all buffered pending output

Example using the reference userspace header:

#include <morse/morse_ioctl.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int fd = open("/dev/morse_dev", O_WRONLY);
int wpm = 20;
ioctl(fd, MORSE_SET_WPM, &wpm);

Configuration

Parameter Default Range How to change
GPIO pin 18 032 ioctl MORSE_SET_PIN
Speed 15 WPM 130 ioctl MORSE_SET_WPM

Morsesh

morsesh is a small interactive terminal that wraps /dev/morse_dev and provides a command-line interface for sending messages and issuing ioctl commands.

Copy the morsesh binary (from result/bin/morsesh) to the Pi - it is statically linked and has no runtime dependencies.

./morsesh

Inside the shell:

morse> send Hello World  # queues "Hello World" for transmission
morse> wpm 20            # sets speed to 20 WPM
morse> pin 18            # sets GPIO pin to 18
morse> flush             # discards buffered output
morse> nonblock          # Toggle non-blocking I/O mode
morse> help              # Show help
morse> quit              # exits

Internals

Source Layout

module/
├── include/
│   ├── morse.h             # shared internal types and constants
│   ├── morse_hal.h         # HAL interface (kernel and stub implementations)
│   └── morse_ioctl.h       # userspace-visible ioctl definitions
├── main.c                  # module init/exit, device registration
├── morse_gpio.c            # GPIO setup and teardown
├── morse_hal_kernel.c      # HAL implementation using kernel GPIO API
├── morse_hal_stub.c        # HAL stub for host unit tests
├── morse_hal_stub.h
├── morse_ioctl.c           # ioctl dispatch
├── morse_tx.c              # encoding and transmit thread
└── tests/
    └── test_morse.c        # CUnit test suite

Architecture

write -> [kfifo buffer] -> [TX thread] -> morse_tx
                                             |
                                        morse_hal.h
                                       /           \
                            morse_hal_kernel.c   morse_hal_stub.c
                            (GPIO hardware)      (unit tests)

The transmit thread is a kernel kthread that sleeps on a wait queue and wakes whenever data is written to the device. It pulls characters from a kfifo, looks up the Morse sequence for each, and calls the HAL to toggle the GPIO pin with the appropriate dot/dash/gap timings derived from the current WPM setting.

The HAL abstraction (morse_hal.h) keeps all hardware access behind a thin interface of three functions; hal_sleep_ms, and hal_set_led

  • making the encoding logic independently testable on the host.

License

Copyright (C) 2026 Mikkel Thestrup
Copyright (C) 2026 Jacob Ziewitz

This project is licensed under the GNU General Public License v2.0. See the LICENSE file for the full license text.