- C 72.6%
- Nix 23.9%
- Typst 1.8%
- Makefile 1.7%
| docs | ||
| module | ||
| morsesh | ||
| questionnaire | ||
| .clang-format | ||
| .clangd | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
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
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
ioctlflush 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 (1–30) |
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 | 0–32 | ioctl MORSE_SET_PIN |
| Speed | 15 WPM | 1–30 | 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.