After writing a Linux kernel module for the 1602A LCD display using GPIO and a character device interface, I wanted to work with serial communication and my first would be UART, the simplest.
This time, I set up bidirectional communication between a Raspberry Pi 4B and an ATmega328p. The Pi acts as a controller, sending commands like "ON" and "OFF" to toggle an LED on the ATmega. In return, the ATmega responds with button state updates like "PRESSED" or "RELEASED" over UART.
Here’s a breakdown of how I made it all work—from device tree overlays to serdev.
I kept the build process minimal but efficient with this Makefile:
make module builds the kernel module (echo.ko).
make dt compiles the device tree source (.dts) into a binary overlay (.dtbo) using dtc.
make all builds both.
make clean cleans everything up.
This overlay binds my kernel driver to uart0, overriding the default serial0 alias:
To load it:
sudo cp uart_Overlay.dtbo /boot/overlays/
Then enable it in /boot/firmware/config.txt:
dtoverlay=uart_Overlay
After reboot, this causes uart0 to instantiate a platform device with the compatible string "decryptec,echo_dev" on boot. serial0 will no longer appear in /dev/ directory
The core module uses the serdev API, a clean abstraction for serial communication in modern Linux kernel development
This links our kernel module to the echodev node defined in the .dts overlay.
I use the probe function to connect the device to the device operations struct, configure the UART, and send a LED on cmd with serdev_device_write_buf function. The remove function sends an LED off cmd and close the serial connection.
I handle incoming data from the ATmega using receive_buf in the serdev_device_ops struct:
It turns on LED pin 13 on when receiving "ON" and stops responding when it receives "OFF". While LED is on, send button state changes.
I was receiving data just fine from the ATmega—but my transmit from the Pi wasn't working at all.
I went through the usual suspects:
Logic level shifter wiring?
Pull-up resistors? Maybe?
RX and TX flipped? Tried both directions.
Weirdest part? If I swapped the TX/RX lines, only one direction would ever work. Either I could send but not receive, or receive but not send.
Turns out... it was the wire.
Yup. Just the wire. Swapping it out fixed everything. Moral of the story? Always validate your hardware.
All in all, this project was a solid introduction in how to write a Device Tree overlay and bind it to a UART peripheral, use the serdev kernel API to send and receive serial data, configure UART settings directly from the kernel, compile and install .dtbo overlays, and use probe/remove and client ops.