Linux内核SPI子系统深度实战:从SPI NOR Flash到自定义外设的完整驱动开发

SPI(Serial Peripheral Interface)是嵌入式系统中最常用的同步串行通信协议之一。从一块小小的SPI NOR Flash芯片到复杂的传感器阵列,SPI以其简单、高速、全双工的特性成为硬件设计的首选接口。然而,Linux内核中的SPI子系统远比表面看起来复杂——它涉及到Controller驱动、Device驱动、Device Tree配置、DMA传输、片选管理等多个层次的协同工作。

本文将从SPI协议的基本原理出发,深入分析Linux内核SPI子系统的架构设计,并通过完整的代码示例展示如何从零编写一个SPI主机控制器驱动和一个SPI设备驱动,最后讨论生产环境中的性能调优和故障排查。

1. SPI协议基础与Linux内核架构总览

1.1 SPI物理层信号

SPI通信需要至少四根信号线:

  • SCLK:串行时钟线,由Master驱动,决定通信速率
  • MOSI:主出从入,Master发送数据到Slave
  • MISO:主入从出,Slave发送数据到Master
  • CS:片选信号,低电平有效,用于选择特定的Slave设备

SPI的四种工作模式由CPOL(时钟极性)和CPHA(时钟相位)组合决定:

Mode CPOL CPHA 空闲时钟 采样边象
0 0 0 低电平 上升沿
1 0 1 低电平 下降沿
2 1 1 高电平 下降沿
3 1 0 高电平 上升沿

绝大多数SPI Flash器件使用Mode 0,而许多ADC/DAC芯片使用Mode 3。理解这个区别对驱动开发至关重要——时钟配置不当是最常见的通信失败原因。

1.2 Linux SPI子系统架构

Linux内核的SPI子系统由三个核心层次构成:

┌─────────────────────────────────────────┐
│            SPI Device Driver            │  ← 设备驱动层(mtd/spi-nor, spi-mem等)
│  (spidev, SPI NOR, sensor, ADC/DAC...) │
├─────────────────────────────────────────┤
│              SPI Core                   │  ← 核心层(spi.c, spi-mem.c)
│   (注册/注销、队列管理、transfer调度)    │
├─────────────────────────────────────────┤
│          SPI Controller Driver          │  ← 控制器驱动层(platform_driver)
│   (BCM2835 SPI, QSPI, Designware...)   │
└─────────────────────────────────────────┘

关键数据结构:

  • spi_master:SPI主机控制器实例,包含操作函数集struct spi_master
  • spi_device:SPI从设备实例,描述一个连接的Slave芯片
  • spi_transfer:一次数据传输的时序描述(tx_buf, rx_buf, len, speed_hz, delay_usecs, bits_per_word, cs_change)
  • spi_message:一个或多个spi_transfer的原子组合,在spi_sync()调用中完成

核心代码位于drivers/spi/spi.c,其中最关键的两个API是:

/* 异步传输(使用完成量同步) */
int spi_sync(struct spi_device *spi, struct spi_message *message);

/* 异步传输(推荐高性能场景) */
int spi_async(struct spi_device *spi, struct spi_message *message);

spi_sync()内部通过spi_async()实现,附加了完成量等待机制。对于SPI Flash这类需要等待命令-响应序列的设备,spi_sync是标准做法。

2. SPI主机控制器驱动开发实战

在嵌入式SoC上,SPI控制器通常作为platform_device出现。编写一个完整的SPI Controller驱动需要实现以下几个关键部分。

2.1 驱动注册流程

#include <linux/spi/spi.h>
#include <linux/platform_device.h>

struct my_spi {
    struct spi_master *master;
    void __iomem *regs;
    struct clk *clk;
    unsigned int freq;
};

static const struct of_device_id my_spi_of_match[] = {
    { .compatible = "vendor,my-spi" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);

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

    /* 1. 分配 spi_master,私有数据大小为 struct my_spi */
    master = spi_alloc_master(&pdev->dev, sizeof(*myspi));
    if (!master)
        return -ENOMEM;

    myspi = spi_master_get_devdata(master);
    myspi->master = master;

    /* 2. 映射寄存器 */
    myspi->regs = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(myspi->regs)) {
        ret = PTR_ERR(myspi->regs);
        goto err_free_master;
    }

    /* 3. 使能时钟 */
    myspi->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(myspi->clk)) {
        ret = PTR_ERR(myspi->clk);
        goto err_free_master;
    }
    clk_prepare_enable(myspi->clk);

    /* 4. 配置 spi_master 结构体 */
    master->bus_num = pdev->id;                    /* SPI bus number */
    master->num_chipselect = 4;                    /* 支持4个片选 */
    master->mode_bits = SPI_CPOL | SPI_CPHA |      /* 支持的模式 */
                        SPI_CS_HIGH | SPI_NO_CS;
    master->setup = my_spi_setup;                  /* 设备初始化回调 */
    master->transfer_one_message = my_spi_transfer; /* 传输函数 */
    master->auto_runtime_pm = true;
    master->max_speed_hz = 50000000;               /* 50MHz max */

    /* 5. 注册 SPI master */
    ret = devm_spi_register_master(&pdev->dev, master);
    if (ret)
        goto err_disable_clk;

    platform_set_drvdata(pdev, myspi);
    return 0;

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

2.2 实现传输函数

传输函数是Controller驱动的核心,它负责将spi_message中的多个translate实际执行:

static int my_spi_transfer(struct spi_master *master,
                           struct spi_message *msg)
{
    struct my_spi *myspi = spi_master_get_devdata(master);
    struct spi_transfer *xfer;
    unsigned int delay_usecs = 0;
    int ret = 0;

    spi_enable_cs(msg->spi);  /* 拉低片选 */

    list_for_each_entry(xfer, &msg->transfers, transfer_list) {
        unsigned int len = xfer->len;
        const u8 *tx_buf = xfer->tx_buf;
        u8 *rx_buf = xfer->rx_buf;

        while (len--) {
            u8 tx_byte = tx_buf ? *tx_buf++ : 0xFF;

            /* 写入TX FIFO并等待完成 */
            writel(tx_byte, myspi->regs + SPI_TXDR);

            /* 等待RX FIFO非空 */
            ret = readl_poll_timeout(myspi->regs + SPI_SR,
                                     readl(myspi->regs + SPI_SR),
                                     readl(myspi->regs + SPI_SR) & SR_RXNE,
                                     1, 1000);
            if (ret)
                goto out;

            /* 读取RX FIFO */
            u8 rx_byte = readl(myspi->regs + SPI_RXDR);
            if (rx_buf)
                *rx_buf++ = rx_byte;
        }

        /* 处理 inter-transfer delay */
        if (xfer->delay_usecs)
            udelay(xfer->delay_usecs);
    }

out:
    if (ret)
        spi_disable_cs(msg->spi);

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

    return ret;
}

2.3 DMA传输优化

对于高速SPI设备(如QSPI Flash在80MHz以上),轮询方式会大量占用CPU。生产级驱动应当集成DMA:

static int my_spi_dma_transfer(struct my_spi *myspi, struct spi_message *msg)
{
    struct dma_async_tx_descriptor *tx_desc, *rx_desc;
    struct spi_transfer *xfer;
    dma_cookie_t tx_cookie, rx_cookie;
    enum dma_ctrl_flags flags = DMA_PREP_INTERRUPT | DMA_CTRL_ACK;

    list_for_each_entry(xfer, &msg->transfers, transfer_list) {
        /* 配置 TX DMA */
        if (xfer->tx_buf) {
            tx_desc = dmaengine_prep_slave_sg(myspi->tx_chan,
                                              xfer->tx_sg.sgl,
                                              xfer->tx_sg.nents,
                                              DMA_MEM_TO_DEV,
                                              flags);
            dmaengine_submit(tx_desc);
            tx_cookie = dmaengine_submit(tx_desc);
            dma_async_issue_pending(myspi->tx_chan);
        }

        /* 配置 RX DMA */
        if (xfer->rx_buf) {
            rx_desc = dmaengine_prep_slave_sg(myspi->rx_chan,
                                              xfer->rx_sg.sgl,
                                              xfer->rx_sg.nents,
                                              DMA_DEV_TO_MEM,
                                              flags);
            dmaengine_submit(rx_desc);
            rx_cookie = dmaengine_submit(rx_desc);
            dma_async_issue_pending(myspi->rx_chan);
        }

        /* 等待DMA完成 */
        wait_for_completion(&myspi->dma_complete);
    }
    return 0;
}

使用DMA传输时,注意cache一致性:对于scatter-gather列表,需要调用dma_map_sg()和dma_unmap_sg()来确保CPU和DMA看到一致的数据。

3. SPI设备驱动开发实战

3.1 Device Tree配置

/* 在SoC设备树中定义SPI设备 */
&spi0 {
    pinctrl-names = "default";
    pinctrl-0 = <&spi0_pins>;
    status = "okay";

    flash: w25q128@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;                    /* CS0 */
        spi-max-frequency = <80000000>; /* 80MHz */
        spi-rx-bus-width = <4>;        /* Quad SPI接收 */
        spi-tx-bus-width = <4>;        /* Quad SPI发送 */
        #address-cells = <1>;
        #size-cells = <1>;

        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;

            boot@0 {
                label = "boot";
                reg = <0x000000 0x100000>; /* 1MB */
            };
            kernel@100000 {
                label = "kernel";
                reg = <0x100000 0x700000>; /* 7MB */
            };
        };
    };

    sensor: bme280@1 {
        compatible = "bosch,bme280";
        reg = <1>;                    /* CS1 */
        spi-max-frequency = <10000000>; /* 10MHz */
        spi-cpol;
        spi-cpha;                     /* Mode 3 */
    };
};

3.2 自定义设备驱动模板

以下是一个完整的温湿度传感器驱动模板,展示了SPI设备驱动的典型结构:

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>

struct my_sensor {
    struct spi_device *spi;
    struct iio_dev *indio_dev;
    s32 temperature;
    u32 humidity;
};

static int my_sensor_read_reg(struct spi_device *spi, u8 reg, u8 *val)
{
    struct spi_transfer xfer[2];
    struct spi_message msg;
    u8 tx[2], rx[2];

    /* 第一阶段:发送寄存器地址(读命令:reg | 0x80) */
    tx[0] = reg | 0x80;
    tx[1] = 0xFF; /* dummy byte */

    spi_message_init(&msg);
    memset(xfer, 0, sizeof(xfer));

    xfer[0].tx_buf = tx;
    xfer[0].len = 1;
    spi_message_add_tail(&xfer[0], &msg);

    xfer[1].tx_buf = &tx[1];
    xfer[1].rx_buf = rx;
    xfer[1].len = 1;
    spi_message_add_tail(&xfer[1], &msg);

    return spi_sync(spi, &msg);
}

static int my_sensor_read_data(struct my_sensor *sensor)
{
    u8 temp_buf[3], hum_buf[2];
    int ret;

    /* 读取温度原始值 */
    ret = my_sensor_read_reg(sensor->spi, REG_TEMP_MSB, &temp_buf[0]);
    if (ret)
        return ret;

    ret = my_sensor_read_reg(sensor->spi, REG_TEMP_LSB, &temp_buf[1]);
    if (ret)
        return ret;

    ret = my_sensor_read_reg(sensor->spi, REG_TEMP_XLSB, &temp_buf[2]);
    if (ret)
        return ret;

    /* 将20-bit原始值转换为毫摄氏度 */
    s32 raw_temp = (temp_buf[0] << 12) | (temp_buf[1] << 4) | (temp_buf[2] >> 4);
    sensor->temperature = calculate_temp(sensor->config, raw_temp);

    /* 读取湿度原始值 */
    ret = my_sensor_read_reg(sensor->spi, REG_HUM_MSB, &hum_buf[0]);
    if (ret)
        return ret;

    ret = my_sensor_read_reg(sensor->spi, REG_HUM_LSB, &hum_buf[1]);
    if (ret)
        return ret;

    u32 raw_hum = (hum_buf[0] << 8) | hum_buf[1];
    sensor->humidity = calculate_humidity(sensor->config, raw_hum);

    return 0;
}

static int my_sensor_read_raw(struct iio_dev *indio_dev,
                               const struct iio_chan_spec *chan,
                               int *val, int *val2, long mask)
{
    struct my_sensor *sensor = iio_priv(indio_dev);
    int ret;

    ret = my_sensor_read_data(sensor);
    if (ret)
        return ret;

    switch (chan->type) {
    case IIO_TEMP:
        *val = sensor->temperature;
        return IIO_VAL_INT;
    case IIO_HUMIDITYRELATIVE:
        *val = sensor->humidity;
        return IIO_VAL_INT;
    default:
        return -EINVAL;
    }
}

static const struct iio_info my_sensor_info = {
    .read_raw = my_sensor_read_raw,
};

static int my_sensor_probe(struct spi_device *spi)
{
    struct my_sensor *sensor;
    struct iio_dev *indio_dev;

    indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*sensor));
    if (!indio_dev)
        return -ENOMEM;

    sensor = iio_priv(indio_dev);
    sensor->spi = spi;
    sensor->indio_dev = indio_dev;

    indio_dev->info = &my_sensor_info;
    indio_dev->name = "my-humidity-sensor";
    indio_dev->channels = my_sensor_channels;
    indio_dev->num_channels = ARRAY_SIZE(my_sensor_channels);
    indio_dev->modes = INDIO_DIRECT_MODE;

    return devm_iio_device_register(&spi->dev, indio_dev);
}

static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "vendor,my-humidity-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

static const struct spi_device_id my_sensor_id[] = {
    { "my-humidity-sensor", 0 },
    { }
};
MODULE_DEVICE_TABLE(spi, my_sensor_id);

static struct spi_driver my_sensor_driver = {
    .driver = {
        .name = "my-humidity-sensor",
        .of_match_table = my_sensor_of_match,
    },
    .probe = my_sensor_probe,
    .id_table = my_sensor_id,
};
module_spi_driver(my_sensor_driver);

4. SPI-MEM子系统:新一代SPI Flash编程接口

Linux 4.19引入了spi-mem子系统,为SPI Flash提供统一的编程接口,取代了直接通过SPI通用的spi_sync()方式操作Flash。

spi-mem的核心思想是将SPI Flash的操作(读、写、擦除、状态寄存器等)抽象为标准的mem_ops接口:

static int spi_nor_exec_op(struct spi_nor *nor, const struct spi_mem_op *op)
{
    struct spi_controller *ctlr = nor->spimaxctlr->master;
    return ctlr->mem_ops.exec_op(nor->spimaxctlr, op);
}

一个典型的SPI Flash读操作可以简洁地表示为:

static const struct spi_mem_op spi_nor_op = {
    .cmd = {
        .opcode = SPINOR_OP_READ_4IO,   /* Fast Read Quad I/O */
        .nbytes = 1,
        .buswidth = SPI_MEM_BUSWIDTH_1_LINE,
    },
    .addr = {
        .nbytes = 4,                     /* 32-bit 地址模式 */
        .val = addr,
        .buswidth = SPI_MEM_BUSWIDTH_4_LINE,
    },
    .dummy = {
        .nbytes = 6,                     /* 6 dummy cycles */
        .buswidth = SPI_MEM_BUSWIDTH_4_LINE,
    },
    .data = {
        .dir = SPI_MEM_DATA_IN,
        .buf.in = buf,
        .len = len,
        .buswidth = SPI_MEM_BUSWIDTH_4_LINE,
    },
};

使用spi-mem后,Controller驱动只需实现exec_op()回调,就能自动支持所有遵循JEDEC标准的SPI Flash。

5. 性能调优与生产经验

5.1 时序参数调优

SPI通信的时序裕度不足是硬件不稳定的常见原因:

  • 时钟频率:超过芯片规格的频率会导致数据错误。建议从10MHz以下开始测试,逐步提升。
  • CS建立时间:CS拉低到第一个时钟沿之间的时间必须满足芯片数据手册要求(通常1-100ns)。
  • CS保持时间:最后一个时钟沿到CS拉高的时间,许多Flash芯片需要至少50ns的保持时间以满足内部编程要求。

Linux SPI框架通过spi_transfer.delay_usecs提供了inter-transfer延迟控制,但CS建立/保持时间通常需要在Controller驱动中精确配置。

5.2 信号完整性问题排查

高速SPI通信失败的排查思路:

  1. 降低时钟频率:如果低频下正常,高频出错→信号完整性问题
  2. 检查走线长度:SPI时钟频率 > 10MHz时,走线长度应 < 10cm
  3. 测量信号质量:用示波器检查过冲、振铃、边沿斜率
  4. 检查地平面:SPI信号线下必须有连续的地平面
  5. 终端电阻:对于长走线,串联22-33Ω电阻靠近Master端可以改善信号质量

5.3 RPM电源管理

现代嵌入式系统中,SPI Controller需要在设备空闲时进入低功耗状态。框架支持运行时电源管理:

#ifdef CONFIG_PM_SLEEP
static int __maybe_unused my_spi_suspend(struct device *dev)
{
    struct my_spi *myspi = dev_get_drvdata(dev);
    spi_master_suspend(myspi->master);
    clk_disable_unprepare(myspi->clk);
    return pinctrl_select_state(mypinctrl, mypinctrl_sleep);
}

static int __maybe_unused my_spi_resume(struct device *dev)
{
    struct my_spi *my_spi = dev_get_drvdata(dev);
    pinctrl_select_state(mypinctrl, mypinctrl_default);
    clk_prepare_enable(my_spi->clk);
    spi_master_resume(my_spi->master);
    return 0;
}
#endif

6. 调试技巧

6.1 启用SPI核心调试日志

# 动态开启SPI传输追踪
echo 'module spi +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module spi_mem +p' >> /sys/kernel/debug/dynamic_debug/control

# 查看SPI设备信息
cat /sys/bus/spi/devices/spi0.0/modalias
cat /sys/bus/spi/devices/spi0.0/of_node/compatible

6.2 使用spidev进行原型验证

在驱动开发完成之前,可以使用spidev字符设备进行原型验证:

# 确保spidev启用
echo spidev 0x00 > /sys/bus/spi/drivers/spidev/bind

# 测试SPI loopback
flash_tst -d /dev/spidev0.0 --dump=id

# 使用spidev_test工具
./spidev_test -D /dev/spidev0.0 -v -s 1000000

6.3 逻辑分析仪抓包

Saleae Logic或DSLogic等USB逻辑分析仪配合sigrok/PulseView软件,可以直接解码SPI时序,是驱动开发最有效的调试工具。

总结

Linux SPI子系统是一个层次分明、架构完善的子系统。掌握spi-master/controller/device三层模型和Transfer/Message机制是编写高质量SPI驱动的基础。在生产环境中:

  • 优先使用spi-mem子系统操作SPI Flash
  • 高频场景务必启用DMA传输
  • 关注信号完整性和时序裕度
  • 善用spidev和逻辑分析仪加速开发调试

SPI虽简单,但要做到高速、稳定、可靠,需要对协议、硬件、内核框架都有深入理解。希望本文能为读者提供一个全面的SPI驱动开发参考。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部