Understanding Linux SPI Drivers from Scratch: A Complete Guide
This article explains the Linux SPI driver framework, layering, device‑tree matching, and data‑transfer process, providing clear examples, key APIs, and step‑by‑step instructions to help beginners grasp SPI communication and resolve common development issues.
In embedded Linux development, SPI is a high‑speed serial bus widely used for Flash, displays, sensors, and ADCs. While many developers know the basic protocol, the Linux SPI driver can seem complex due to its layered architecture, bus matching, and device‑tree configuration.
1. Review of the SPI Bus Protocol
1.1 What is SPI?
SPI (Serial Peripheral Interface) is a full‑duplex, synchronous communication bus developed by Motorola in the 1980s, used to connect microcontrollers with peripherals such as sensors, memory, and displays.
SPI follows a master‑slave model: the master (e.g., a microcontroller) generates the clock (SCK) and initiates transfers, while slaves respond to the clock and exchange data.
The hardware requires only four lines—SCK, MOSI, MISO, and SS/CS—saving pins and simplifying PCB layout.
1.2 SPI Hardware Signals
MISO carries data from slave to master, MOSI carries data from master to slave, SCK provides the timing, and SS/CS selects the target slave.
1.3 SPI Communication Process
Data transfer uses shift registers. For example, when the master sends 0x55 and the slave sends 0xAA, the communication proceeds through four stages:
Chip‑select stage: the master pulls the target SS/CS low.
Clock and data stage: the master generates SCK; on each edge, a bit is shifted out via MOSI and a bit is received via MISO.
Data reception stage: after 8 clock cycles, both sides have exchanged one byte.
End stage: the master releases SS/CS.
2. Linux SPI Driver Architecture
2.1 Layered Design
The driver consists of three layers: the core layer, the master‑controller driver layer, and the device driver layer. The core provides a unified API to device drivers and abstracts the hardware specifics of different master controllers.
The master‑controller driver handles low‑level controller registers and signal generation, while the device driver focuses on a specific peripheral (e.g., a temperature sensor) and uses the core API to communicate.
2.2 Core Layer Functions
Key APIs include: spi_register_driver(struct spi_driver *sdrv): registers a device driver with the kernel. spi_sync(struct spi_device *spi, struct spi_message *msg): performs a synchronous transfer, blocking until completion.
// Example of driver registration
static struct spi_driver spi_sensor_driver = {
.probe = sensor_probe,
.remove = sensor_remove,
.driver = {
.name = "spi_sensor",
.owner = THIS_MODULE,
},
};
module_init(sensor_init);
module_exit(sensor_exit);2.3 Master‑Controller (spi_master) Structure
The spi_master structure describes the controller’s capabilities: bus_num: unique identifier for the controller. num_chipselect: number of chip‑select lines supported. setup: function pointer to configure device parameters (mode, speed, etc.). transfer: function pointer that performs the actual data transfer.
// Define master number and chip‑select count
.bus_num = 0,
.num_chipselect = 4,
.setup = my_spi_setup,
.transfer = my_spi_transfer,2.4 Device Driver (spi_driver) Essentials
The spi_driver structure links a driver to the core: id_table: array of supported device IDs. probe: called after a successful match to initialize the device.
// Device ID table example
static const struct spi_device_id spi_sensor_ids[] = {
{ "temp_sensor", 0 },
{ "imu_sensor", 1 },
{},
};
MODULE_DEVICE_TABLE(spi, spi_sensor_ids);3. Matching Mechanisms
3.1 Device‑Tree Matching
In Device‑Tree‑based systems, each SPI device has a node with a compatible property. The driver provides an of_match_table to match these strings.
spi1: spi@02100000 {
compatible = "fsl,imx6ul-ecspi", "fsl,imx6q-ecspi";
reg = <0x02100000 0x4000>;
status = "okay";
flash: flash@0 {
compatible = "micron,n25q128a13ef";
reg = <0>;
spi-max-frequency = <50000000>;
};
};The driver’s OF table:
static const struct of_device_id my_spi_flash_of_match[] = {
{ .compatible = "micron,n25q128a13ef" },
{},
};
MODULE_DEVICE_TABLE(of, my_spi_flash_of_match);3.2 ACPI Matching
On x86 platforms, ACPI IDs (HID/CID) are used. The driver defines an acpi_device_id array.
static const struct acpi_device_id my_spi_acpi_ids[] = {
{ "ACPI0001", 0 },
{},
};
MODULE_DEVICE_TABLE(acpi, my_spi_acpi_ids);3.3 Traditional ID Matching
If no Device‑Tree or ACPI is present, the kernel matches the device’s modalias against the driver’s id_table.
4. Practical Example: Temperature Sensor Driver
4.1 Hardware and Kernel Preparation
Verify the four SPI signals (SCK, MOSI, MISO, SS/CS) and power connections. Use a multimeter to check continuity and avoid shorts.
Enable the SPI subsystem and the specific controller in make menuconfig, optionally turning on debugging or DMA support.
4.2 Define ID Tables and OF Match
static const struct spi_device_id my_temp_sensor_ids[] = {
{ "my_temp_sensor", 0 },
{},
};
MODULE_DEVICE_TABLE(spi, my_temp_sensor_ids);
static const struct of_device_id my_temp_sensor_of_match[] = {
{ .compatible = "vendor,my-temp-sensor" },
{},
};
MODULE_DEVICE_TABLE(of, my_temp_sensor_of_match);4.3 Implement Probe and Data Transfer
static int my_temp_sensor_probe(struct spi_device *spi)
{
struct my_temp_sensor_private *priv;
priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
spi_set_drvdata(spi, priv);
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 1000000;
ret = spi_setup(spi);
if (ret) {
dev_err(&spi->dev, "spi setup failed: %d
", ret);
return ret;
}
// Additional hardware init here
return 0;
}Reading temperature data:
struct spi_transfer tr = {
.tx_buf = NULL,
.rx_buf = priv->rx_buffer,
.len = sizeof(priv->rx_buffer),
};
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&tr, &msg);
ret = spi_sync(spi, &msg);
if (ret) {
dev_err(&spi->dev, "SPI read failed: %d
", ret);
return ret;
}
uint16_t temp_raw = (priv->rx_buffer[0] << 8) | priv->rx_buffer[1];
float temperature = convert_temp(temp_raw);4.4 Register Driver and Device‑Tree Node
static struct spi_driver my_temp_sensor_driver = {
.driver = {
.name = "my_temp_sensor_driver",
.owner = THIS_MODULE,
.of_match_table = my_temp_sensor_of_match,
},
.id_table = my_temp_sensor_ids,
.probe = my_temp_sensor_probe,
.remove = my_temp_sensor_remove,
};
module_init(my_temp_sensor_init);
static int __init my_temp_sensor_init(void)
{
return spi_register_driver(&my_temp_sensor_driver);
}Device‑Tree snippet for the sensor:
spi1: spi@02100000 {
compatible = "fsl,imx6ul-ecspi", "fsl,imx6q-ecspi";
reg = <0x02100000 0x4000>;
status = "okay";
temp_sensor: temp_sensor@0 {
compatible = "vendor,my-temp-sensor";
reg = <0>;
spi-max-frequency = <1000000>;
};
};4.5 Build, Debug, and Optimize
Makefile example:
obj-m += my_temp_sensor_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) cleanAfter building, load the module and use dmesg | grep spi to inspect kernel logs. Add printk statements for deeper debugging.
Performance can be improved by selecting appropriate SPI mode and clock, minimizing memory copies, and ensuring proper resource management to avoid leaks and contention.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
