A Simple Analysis of the Linux SPI Control Layer

Introduction

  1. I previously shared the control layer for I2C and mentioned that I would update the SPI section shortly. However, I was too tired and didn’t get around to it, so I apologize for the delay, haha.
  2. The SPI protocol is much simpler than I2C, but I find the software handling to be more complex than I2C. This is because SPI involves throughput issues, which leads to the use of linked list structures. Additionally, there is the handling of chip select signals, making the software aspect somewhat more complicated compared to I2C.
  3. The most frustrating part of analyzing the SPI control layer is that NXP does not use the standard hardware SPI structure but instead uses a bitbang structure (software SPI). This may be for compatibility with older versions of chips that do not have hardware SPI.
  4. As for the reasons behind this, we do not know. I will not introduce bitbang-related content here. Instead, I will only discuss how to write a standard hardware SPI control layer. Therefore, we only need to look at the SPI control layer source code provided by ST or RK.

Main Content

Core Structures

  1. spi_controller (spi_master): Abstracts the SPI controller. Below are some important members:
  • dev.of_node: Used in conjunction with the device tree to obtain device configuration from the device tree. Generally assigned as pdev->dev.of_node;
  • transfer, transfer_one, transfer_one_message: All involve data transmission. transfer is a high-level function that is useful for simple drivers that do not use the kernel-provided queue mechanism, which is generally not implemented in newer kernel versions. transfer_one is the low-level implementation of transfer_one_message. Implementing only transfer_one is more suitable for complete control of message transmission. If you choose to implement only transfer_one, you need to rely on the scheduling and queue management mechanisms provided by the SPI core framework, which will automatically decompose messages and call transfer_one for each segment’s processing.
  • setup: Initializes and configures the SPI device with necessary parameters before data transmission, such as clock rate, communication mode, data bit width, etc. This is because a single SPI controller may be connected to devices with different communication modes, clock rates, and data bit widths. Additionally, the chip select signal needs to be set at the beginning of communication.
  • bus_num: The SPI bus number, where a positive value indicates a specific bus number and a negative value indicates dynamic allocation.
  • num_chipselect: Many SPI controllers internally inherit chip select signals, which indicates the number of chip select signals within the controller.
  • max_speed_hz and min_speed_hz: The maximum and minimum values of the SPI transmission rate.
  • mode_bits: Defines the operation modes supported by the controller, such as SPI_MODE_0, SPI_MODE_1, etc. Specific mode parameters can include polarity, high and low levels, etc.
  • bits_per_word_mask: A bitmask representing the data bit widths supported by the SPI controller. This is expressed as a bitmask indicating the number of bits the controller can support per word. For example, if the controller supports 8-bit and 16-bit transmissions, bits_per_word_mask may be set to a bitmask representing these two widths.
// kernel/include/linux/spi/spi.h
/* Compatibility layer */
#define spi_master          spi_controller
  1. spi_device: Describes an SPI slave device.
  2. spi_driver: Describes an SPI device driver.
  3. spi_transfer: Describes the specific data for SPI transmission.
  4. spi_message: Describes the information for a single SPI transmission.

Control Registration Process Introduction

  1. Method 1:
  • Request spi_controller: spi_alloc_master
  • Obtain device tree information, perform related hardware configurations, and initialize some important parameters in the spi_controller structure.
  • Register spi_controller: devm_spi_register_master (IMX calls spi_bitbang_start, which essentially calls devm_spi_register_master).
  1. Method 2: IMX6ULL uses the bitbang mechanism to implement SPI transmission. Bitbang can simulate SPI through software control of GPIO. This may be to maintain compatibility with older versions of chips that do not have SPI. However, the software-simulated SPI can use the program provided by the Linux kernel in kernel/drivers/spi/spi-gpio.c.

Minimal SPI Control Layer Code

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/clk.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/platform_device.h>

#define MY_SPI_MAX_SPEED_HZ 50000000  // Maximum SPI speed 50MHz

struct my_spi {
    struct spi_master *master;
    void __iomem *base; // I/O memory base address
    struct clk *clk; // Clock
    int irq;  // Interrupt number
};

static int my_spi_setup(struct spi_device *spi)
{
    // Set related parameters for the SPI device here.
    return 0;
}

static int my_spi_transfer_one(struct spi_master *master, struct spi_message *msg)
{
    struct spi_device *spi = msg->spi;
    struct my_spi *myspi = spi_master_get_devdata(master);
    struct spi_transfer *xfer;
    u8 *tx_buf, *rx_buf;
    
    list_for_each_entry(xfer, &msg->transfers, transfer_list) {
        tx_buf = xfer->tx_buf;
        rx_buf = xfer->rx_buf;

        // Simplified SPI transmission logic, assuming only one byte
        if (tx_buf) {
            iowrite8(*tx_buf, myspi->base);
        }

        if (rx_buf) {
            *rx_buf = ioread8(myspi->base);
        }

        msg->actual_length += xfer->len;
    }

    msg->status = 0;
    spi_finalize_current_message(master);

    return 0;
}

static int my_spi_probe(struct platform_device *pdev)
{
    struct spi_master *master;
    struct my_spi *myspi;
    struct resource *res;
    int ret;

    // Allocate SPI master structure
    master = spi_alloc_master(&pdev->dev, sizeof(struct my_spi));
    if (!master)
        return -ENOMEM;

    platform_set_drvdata(pdev, master);
    myspi = spi_master_get_devdata(master);

    // Get hardware resources
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    myspi->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(myspi->base)) {
        ret = PTR_ERR(myspi->base);
        goto err_free_master;
    }

    myspi->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(myspi->clk)) {
        ret = PTR_ERR(myspi->clk);
        goto err_free_master;
    }

    ret = clk_prepare_enable(myspi->clk);
    if (ret)
        goto err_free_master;

    myspi->irq = platform_get_irq(pdev, 0);
    if (myspi->irq < 0) {
        ret = myspi->irq;
        goto err_clk_disable;
    }

    // SPI master configuration
    master->bus_num = -1;  // Automatically allocate bus number
    master->num_chipselect = 2;
    master->setup = my_spi_setup;
    master->transfer_one = my_spi_transfer_one;
    master->max_speed_hz = MY_SPI_MAX_SPEED_HZ;
    master->mode_bits = SPI_MODE_0;

    ret = spi_register_master(master);
    if (ret)
        goto err_clk_disable;

    dev_info(&pdev->dev, "SPI driver initialized\n");
    return 0;

err_clk_disable:
    clk_disable_unprepare(myspi->clk);
err_free_master:
    spi_master_put(master);
    return ret;
}

static int my_spi_remove(struct platform_device *pdev)
{
    struct spi_master *master = platform_get_drvdata(pdev);
    struct my_spi *myspi = spi_master_get_devdata(master);

    spi_unregister_master(master);
    clk_disable_unprepare(myspi->clk);

    return 0;
}

static const struct of_device_id my_spi_dt_ids[] = {
    { .compatible = "myvendor,my-spi", },
    { /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, my_spi_dt_ids);

static struct platform_driver my_spi_driver = {
    .driver = {
        .name = "my_spi",
        .of_match_table = my_spi_dt_ids,
    },
    .probe = my_spi_probe,
    .remove = my_spi_remove,
};

module_platform_driver(my_spi_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple SPI controller driver");

Leave a Comment