SPI Device Tree Registration Guide
Free Linux Device Drivers Course — Part 8.4: Instantiating SPI Devices and Registering SPI Drivers on Kernel 6.x
free linux device drivers course
free linux kernel development course
free embedded systems course
free embedded linux course
This lecture is part of our free Linux device drivers course and continues the SPI chapter with the topic of SPI device tree registration — how an SPI device actually becomes a struct spi_device that your driver’s probe() function receives. In the previous lecture we studied the probe()/remove() lifecycle of an SPI protocol driver. Here we step back one level and answer a simpler but often confusing question: how does the kernel know an SPI device exists in the first place?
What You Will Learn
- How SPI devices were historically instantiated using board files (deprecated method)
- How modern kernels instantiate SPI devices using the device tree
- The meaning of the
regproperty on an SPI child node - Standard SPI device tree properties:
spi-max-frequency,spi-cpol,spi-cpha,spi-cs-high - How to define and register a complete
spi_driverthat matches a device tree node - Kernel 6.x modernizations: the
spi_controllerrename and thespi_get_chipselect()accessor - A full original demo driver you can build and load on real hardware
Prerequisites
- Completion of the earlier SPI lectures in this free linux kernel development course (SPI fundamentals, struct spi_device, probe/remove lifecycle)
- Basic device tree knowledge (nodes, compatible strings, of_device_id matching) — covered in our device tree chapter
- A Linux board or SBC with an SPI bus enabled (Raspberry Pi, BeagleBone, or any SoC with device tree support)
- Kernel headers installed for building out-of-tree modules
Quick Recap: What Is SPI?
SPI (Serial Peripheral Interface) is a synchronous, full-duplex bus used to connect a controller to one or more peripheral chips such as flash memory, ADCs, DACs, sensors, and displays. Every SPI transaction is clocked by the controller — peripherals never generate the clock themselves.
| Signal | Purpose |
|---|---|
| SCK (Serial Clock) | Clock generated by the controller for every bit transferred |
| MOSI (Master Out Slave In) | Data sent from controller to peripheral |
| MISO (Master In Slave Out) | Data sent from peripheral to controller |
| CS / SS (Chip Select) | Selects which peripheral is active on a shared bus, usually active-low |
Because one controller can drive several peripherals, each peripheral needs its own chip-select line. That single fact — “which chip select does this device use?” — is exactly the piece of information the kernel needs before it can create a struct spi_device for your driver to bind to. This is where device instantiation comes in.
Two Ways to Instantiate an SPI Device
struct spi_board_info → spi_register_board_info() → kernel builds struct spi_device
struct spi_device automatically at controller registration time
In both cases the destination is identical: a populated struct spi_device that the SPI core matches against a registered spi_driver. Only the source of that information changes.
The Old Way: Board Files
Before device tree existed, every board had a hand-written C file under arch/<architecture>/mach-<board>/ that described all the hardware present on that board, including SPI devices. This is only relevant today on systems that still boot without a device tree, which is now rare. It is worth understanding for legacy code you may encounter, but you should not write new drivers this way.
struct my_platform_data {
int foo;
bool bar;
};
static struct my_platform_data mpfd = {
.foo = 15,
.bar = true,
};
static struct spi_board_info my_board_spi_info[] __initdata = {
{
.modalias = "ep-spi-demo", /* must match spi_driver name */
.max_speed_hz = 1000000,
.bus_num = 0,
.chip_select = 0,
.platform_data = &mpfd,
.mode = SPI_MODE_0,
},
};
static int __init board_init(void)
{
return spi_register_board_info(my_board_spi_info,
ARRAY_SIZE(my_board_spi_info));
}
Why it is deprecated: board files hard-code hardware details in kernel C source, which means a new board needs a kernel rebuild. Device tree separates hardware description from kernel code, so the same kernel image can support many boards. On current kernels, spi_register_board_info() still exists in the SPI core for backward compatibility, but almost no active driver relies on it — device tree (or ACPI on x86) has replaced it everywhere.
The Modern Way: Device Tree
An SPI device is declared as a child node of its SPI controller node in the device tree. The controller node already defines #address-cells = <1> and #size-cells = <0>, which tells the parser that the child’s reg property is a single one-cell address with no size — that single cell is the chip select index, not a memory address.
&spi0 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
ep_spi_demo: ep-spi-demo@0 {
compatible = "embeddedpathashala,ep-spi-demo";
reg = <0>; /* chip select 0 */
spi-max-frequency = <4000000>; /* 4 MHz cap */
spi-cpha; /* clock phase shifted */
ep,sample-rate = <100>; /* custom application property */
};
};
| Property | Meaning |
|---|---|
reg |
Chip select index on the parent bus, counted from 0 |
spi-max-frequency |
Maximum SPI clock in Hz the device tolerates; the core enforces this automatically |
spi-cpol |
Boolean; device needs inverted clock polarity (CPOL = 1) |
spi-cpha |
Boolean; device needs shifted clock phase (CPHA = 1) |
spi-cs-high |
Boolean; device needs chip select active-high instead of the default active-low |
| Aspect | Board File (Old) | Device Tree (New) |
|---|---|---|
| Location of data | Kernel C source in arch/ | .dts / .dtsi source, compiled to .dtb |
| Kernel rebuild needed for new board | Yes | No — swap the .dtb / overlay |
| Structure used | struct spi_board_info |
Device tree node parsed by OF core |
| Registration call | spi_register_board_info() |
Automatic, via of_register_spi_devices() when the controller registers |
| Status on kernel 6.x | Present but deprecated for new use | Standard, recommended method |
Defining and Registering the SPI Driver
Once the device tree node exists, the kernel needs a matching spi_driver to bind to it. The pattern has three parts: a match table, the probe/remove callbacks, and the driver structure itself.
/* 1. Device tree match table */
static const struct of_device_id ep_spi_of_match[] = {
{ .compatible = "embeddedpathashala,ep-spi-demo" },
{ }
};
MODULE_DEVICE_TABLE(of, ep_spi_of_match);
/* 2. Fallback ID table (used when matching by name, not by DT) */
static const struct spi_device_id ep_spi_id[] = {
{ "ep-spi-demo", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, ep_spi_id);
/* 3. Driver structure */
static struct spi_driver ep_spi_driver = {
.driver = {
.name = "ep_spi_dtreg_demo",
.of_match_table = ep_spi_of_match,
},
.probe = ep_spi_probe,
.remove = ep_spi_remove,
.id_table = ep_spi_id,
};
module_spi_driver(ep_spi_driver);
MODULE_DEVICE_TABLE() exports the compatible string so that user-space module loading tools (udev/modprobe) can auto-load your module when a matching device tree node is present. module_spi_driver() is a helper macro that generates the boilerplate module_init()/module_exit() pair for a driver that has no special init/exit needs — you saw this same helper in the earlier probe/remove lecture.
Kernel 6.x Modernizations to Know
| Old API | Current API | Why it changed |
|---|---|---|
struct spi_master |
struct spi_controller |
SPI core generalized “master” terminology to “controller”; compatibility aliases still exist for old drivers |
spi->master |
spi->controller |
Field renamed alongside the struct rename |
spi->chip_select (direct read) |
spi_get_chipselect(spi, idx) |
Chip select became an array to support multi-CS memory devices; direct field access is no longer safe |
of_property_read_bool() only |
device_property_read_bool() / device_property_present() |
Firmware-agnostic API that works identically for device tree and ACPI |
Complete Example: ep_spi_dtreg_demo
This original driver demonstrates the full path: a device tree node is matched, the driver’s probe() runs, and it reads back both standard and custom properties.
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/property.h>
static int ep_spi_probe(struct spi_device *spi)
{
u32 sample_rate = 0;
bool cpha_set;
dev_info(&spi->dev, "ep_spi_dtreg_demo: probing device\n");
dev_info(&spi->dev, " chip select : %d\n", spi_get_chipselect(spi, 0));
dev_info(&spi->dev, " max speed : %u Hz\n", spi->max_speed_hz);
dev_info(&spi->dev, " controller : spi%d\n", spi->controller->bus_num);
cpha_set = device_property_read_bool(&spi->dev, "spi-cpha");
dev_info(&spi->dev, " cpha requested : %s\n", cpha_set ? "yes" : "no");
if (device_property_read_u32(&spi->dev, "ep,sample-rate", &sample_rate) == 0)
dev_info(&spi->dev, " ep,sample-rate = %u\n", sample_rate);
return 0;
}
static void ep_spi_remove(struct spi_device *spi)
{
dev_info(&spi->dev, "ep_spi_dtreg_demo: removing device\n");
}
static const struct of_device_id ep_spi_of_match[] = {
{ .compatible = "embeddedpathashala,ep-spi-demo" },
{ }
};
MODULE_DEVICE_TABLE(of, ep_spi_of_match);
static const struct spi_device_id ep_spi_id[] = {
{ "ep-spi-demo", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, ep_spi_id);
static struct spi_driver ep_spi_driver = {
.driver = {
.name = "ep_spi_dtreg_demo",
.of_match_table = ep_spi_of_match,
},
.probe = ep_spi_probe,
.remove = ep_spi_remove,
.id_table = ep_spi_id,
};
module_spi_driver(ep_spi_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Demo SPI driver showing device tree registration");
Build and Test Workflow
Save the DT snippet shown earlier as an overlay source file, and the driver code above as ep_spi_dtreg_demo.c.
# Compile the device tree overlay
dtc -@ -I dts -O dtb -o ep-spi-demo.dtbo ep-spi-demo-overlay.dts
# Copy it to the firmware overlays directory (path varies by board)
sudo cp ep-spi-demo.dtbo /boot/firmware/overlays/
# Apply it (or add it to config.txt / your bootloader config and reboot)
sudo dtoverlay ep-spi-demo
# Build the kernel module
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# Load the module
sudo insmod ep_spi_dtreg_demo.ko
# Check the kernel log
dmesg | tail -n 10
Expected dmesg output:
ep_spi_dtreg_demo: probing device
chip select : 0
max speed : 4000000 Hz
controller : spi0
cpha requested : yes
ep,sample-rate = 100
# Unload
sudo rmmod ep_spi_dtreg_demo
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Missing #address-cells/#size-cells on the SPI controller node |
Device tree compiler warnings, device not created | Ensure the parent SPI controller node sets these to 1 and 0 |
Wrong reg value |
Device probes on the wrong chip select, bus conflicts | Match reg to the physical CS line wired on the board |
Using spidev directly in a production device tree |
Kernel logs a “buggy DT” warning | Write a dedicated compatible string and driver instead of binding raw spidev |
Reading spi->chip_select directly on newer kernels |
Compiler warnings or incorrect value with multi-CS controllers | Use spi_get_chipselect(spi, 0) |
Forgetting MODULE_DEVICE_TABLE() |
Module loads manually but never auto-loads via udev | Always export both the of and spi id tables |
Best Practices
- Always prefer device tree over board files for any board built after ~2013
- Keep
spi-max-frequencyconservative until you have verified signal integrity at higher speeds - Use
device_property_*calls instead ofof_property_*so your driver also works on ACPI-based systems without changes - Export both an
of_device_idand aspi_device_idtable for maximum compatibility
Performance and Security Considerations
Performance: the SPI core enforces spi-max-frequency in hardware-relevant paths, so setting it too low needlessly caps throughput, while setting it above the device’s datasheet rating causes corrupted transfers. Security: validate any data read back from an SPI peripheral in your driver before exposing it to user space through sysfs or a character device — a compromised or faulty peripheral should never be able to crash the kernel or corrupt driver-internal state.
Real-World Use Cases
- SPI NOR/NAND flash storage declared as device tree children of the SPI controller
- TFT display controllers and e-paper panels bound via compatible strings
- ADC/DAC front-ends in industrial and measurement equipment
- TPM security chips connected over SPI
Summary and Key Takeaways
- SPI devices are represented in the kernel as
struct spi_device, built either from a legacy board file or from a device tree node - Board files are deprecated and only relevant on systems without device tree support
- The device tree
regproperty on an SPI child node means chip select index, not a memory address - Standard SPI DT properties (
spi-max-frequency,spi-cpol,spi-cpha,spi-cs-high) configure bus timing and polarity automatically - A complete driver needs an
of_device_idtable, an optionalspi_device_idtable, and a registeredspi_driver - Kernel 6.x renamed
spi_mastertospi_controllerand moved chip-select access behindspi_get_chipselect()
Conclusion
Understanding SPI device tree registration closes the gap between “I wrote a probe function” and “my driver actually gets called.” Once you can read and write the device tree node, choose the right standard properties, and register a matching spi_driver, you have everything needed to bring up real SPI peripherals on modern Linux boards. The next lecture in this free Linux device drivers course builds on this foundation with userspace SPI access using spidev, followed by half-duplex and full-duplex transfer patterns in real driver code.
Frequently Asked Questions
Is spi_register_board_info() still available in current kernels?
Yes, the function still exists in the SPI core for backward compatibility, but it is considered legacy. New drivers and boards should use device tree (or ACPI on x86 systems) instead.
What does the reg property mean for an SPI device tree node?
It specifies the chip select index on the parent SPI controller, starting from 0 — it is not a memory address, unlike reg properties on memory-mapped platform devices.
Do I need both an of_device_id table and a spi_device_id table?
The of_device_id table is required for device tree matching. The spi_device_id table is optional but recommended as a fallback and for exposing MODULE_DEVICE_TABLE() metadata to module-loading tools.
What changed with spi_master versus spi_controller?
The SPI core renamed spi_master to spi_controller to use more inclusive terminology. Compatibility macros exist for older drivers, but new code should use struct spi_controller and the spi->controller field.
Why can’t I read spi->chip_select directly anymore?
Chip select support was extended to arrays for multi-chip-select memory devices, so direct field access is unreliable on newer kernels. Use the spi_get_chipselect(spi, index) accessor instead.
What is the difference between spi-cpol and spi-cpha?
spi-cpol sets clock polarity (idle clock level), and spi-cpha sets clock phase (which clock edge data is sampled on). Together they define one of the four standard SPI modes.
Can one SPI driver handle devices from both device tree and legacy board files?
Yes, as long as the driver exposes both an of_device_id table and a spi_device_id table, the SPI core can match the device regardless of which instantiation method created it.
Where can I practice this for free?
This lecture is part of a free Linux device drivers course and free embedded Linux course on EmbeddedPathashala, covering device tree, character drivers, platform drivers, and SPI/I2C client drivers end to end.
Continue the Free Linux Device Drivers Course
Next up: userspace SPI access with spidev, and half-duplex vs full-duplex transfers.
