4、SPI控制器驱动分析:sun6i-spi驱动源码结构、probe流程、传输队列管理

全志T113的SPI控制器驱动,内核里叫sun6i-spi。名字带"sun6i",是因为它最早用在A31(Sun6i)平台,后来一路继承到T113上。我刚开始看这个驱动时,第一反应是——代码量不大,但设计得很紧凑。

说白了,这就是一个典型的SPI主控驱动。它要完成三件事:初始化硬件、管理传输队列、处理中断。咱们今天就把这三件事拆开揉碎,讲清楚。

4.1 驱动源码结构:文件不多,五脏俱全

驱动代码在drivers/spi/spi-sun6i.c里。整个文件大概1500行,结构很清晰。我个人习惯把驱动分成几个逻辑块来看:

模块对应函数/结构职责
平台驱动入口sun6i_spi_probe / remove设备树匹配、资源申请、硬件初始化
传输引擎sun6i_spi_transfer_one单次SPI传输的启动与完成
队列管理sun6i_spi_queue / sun6i_spi_dequeue消息入队、出队、调度
中断处理sun6i_spi_handlerFIFO读写、传输完成、错误处理
硬件寄存器sun6i_spi_regmap 宏定义寄存器偏移、位域定义

嗯,这里要注意:驱动里没有用传统的master->transfer回调,而是用了master->transfer_one。这是SPI子系统后来推荐的方式,支持异步传输和队列管理。

我的经验: 如果你在移植这个驱动到其他全志芯片,重点看sun6i_spi_regmap里的寄存器定义。不同芯片的FIFO深度、时钟分频方式可能有差异,但整体框架几乎不用动。

4.2 probe流程:从设备树到硬件就绪

probe函数是驱动的起点。我把它拆成6个关键步骤,每一步都有坑:

  1. 获取设备树资源:读取reg、interrupts、clocks、dmas等属性。T113的SPI控制器有DMA支持,但probe里只是获取DMA通道,实际使用在传输时。
  2. 映射寄存器地址:devm_ioremap_resource,把物理地址转成虚拟地址。我遇到过地址重叠导致映射失败的情况,后来发现是设备树里reg写错了范围。
  3. 时钟使能与复位:clk_prepare_enable打开SPI模块时钟,reset_control_deassert释放复位。这一步顺序不能反——先时钟后复位,否则硬件可能挂死。
  4. 配置SPI控制器:设置主模式、关闭中断、清空FIFO。初始状态下的控制器是"干净"的。
  5. 注册SPI主控:spi_register_master,把master结构体注册到SPI子系统。注册成功后,系统会自动扫描设备树中的SPI从设备节点。
  6. 申请中断:devm_request_irq,注册中断处理函数。注意这里用的是IRQF_SHARED标志,因为有些全志芯片的SPI中断线是共享的。
核心代码片段:probe中的关键调用
static int sun6i_spi_probe(struct platform_device *pdev)
{
    struct spi_master *master;
    struct sun6i_spi *sspi;
    int ret;

    // 1. 分配master结构体
    master = spi_alloc_master(&pdev->dev, sizeof(struct sun6i_spi));
    if (!master)
        return -ENOMEM;

    // 2. 设置master参数
    master->bus_num = pdev->id;
    master->num_chipselect = 1;
    master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
    master->transfer_one = sun6i_spi_transfer_one;
    master->setup = sun6i_spi_setup;

    // 3. 获取硬件资源
    sspi = spi_master_get_devdata(master);
    sspi->base = devm_platform_ioremap_resource(pdev, 0);
    sspi->irq = platform_get_irq(pdev, 0);

    // 4. 时钟与复位
    sspi->clk = devm_clk_get(&pdev->dev, NULL);
    clk_prepare_enable(sspi->clk);
    reset_control_deassert(sspi->rstc);

    // 5. 硬件初始化
    sun6i_spi_hw_init(sspi);

    // 6. 注册主控
    ret = devm_spi_register_master(&pdev->dev, master);
    if (ret)
        goto err_free_master;

    return 0;
}

我曾经在调试时发现probe成功了,但SPI传输就是没反应。查了半天,原来是reset_control_deassert之后没有加延时,硬件还没稳定就开始配置寄存器。加个udelay(10)就解决了。

4.3 传输队列管理:消息来了怎么走?

SPI传输的核心是队列管理。全志的SPI控制器只有一个硬件FIFO,但上层可能同时下发多个spi_message。驱动必须把这些消息排队,一个一个处理。

驱动里用了一个struct list_head作为队列头,每个spi_message通过msg->queue链接。流程是这样的:

  1. 消息入队:spi_queued_transfer把消息挂到master的队列上,然后调用sun6i_spi_start_transfer尝试启动传输。
  2. 传输启动:如果当前没有正在进行的传输,就从队列头部取出消息,配置硬件寄存器,写入FIFO,开启中断。
  3. 中断处理:每次中断处理函数检查FIFO状态,读走接收数据,写入发送数据。如果消息传输完成,调用spi_finalize_current_message通知上层。
  4. 队列调度:当前消息完成后,检查队列是否还有待处理消息。有的话,继续启动下一个。
注意: 全志SPI控制器的FIFO深度只有64字节(T113是64,有些老芯片是32)。如果单次传输超过FIFO深度,驱动必须用中断方式分批读写。我曾经遇到过一个大块数据传输丢数据的问题,就是因为中断处理函数里没有正确处理FIFO半满标志。

驱动里有个关键函数sun6i_spi_fill_fifo,它负责往FIFO里填数据。逻辑很简单:

static void sun6i_spi_fill_fifo(struct sun6i_spi *sspi)
{
    u32 len = min(sspi->fifo_depth, sspi->len);
    u8 *buf = sspi->tx_buf;

    for (int i = 0; i < len; i++) {
        if (sspi->tx_buf)
            writel(buf[i], sspi->base + SUN6I_TXDATA_REG);
        else
            writel(0, sspi->base + SUN6I_TXDATA_REG);
    }

    sspi->len -= len;
    if (sspi->tx_buf)
        sspi->tx_buf += len;
}

嗯,这里有个细节:如果tx_buf是NULL,说明是只读传输(比如读取SPI Flash ID),驱动会自动填充0。全志的硬件在发送0时也会产生时钟,所以接收端能正常采样。

4.4 传输队列的同步与互斥

队列管理最怕的是竞态。驱动里用了spin_lock_irqsave来保护队列操作。为什么用spinlock而不是mutex?因为中断处理函数里也要操作队列,mutex会导致睡眠,在中断上下文里不能用。

我画了一张流程图,把整个传输队列的流转关系展示出来:

SPI传输队列管理流程图 spi_message 入队 检查是否正在传输 启动传输 (填FIFO) 中断处理 (读/写FIFO) 传输完成 → 通知上层 检查队列 → 继续/等待 队列非空时循环 关键点 • 使用 spinlock 保护队列 • 中断上下文不能睡眠 • FIFO深度64字节

你想想看,如果队列里同时有5个消息,每个消息包含多个spi_transfer段,驱动必须保证每个段按顺序执行,不能乱。全志的驱动实现里,sun6i_spi_transfer_one只处理一个spi_transfer,完成后通过回调触发下一个。这种设计叫"流水线式处理",简单可靠。

避坑指南: 我曾经在调试SPI NAND Flash时发现,连续读写大块数据时偶尔会卡死。最后定位到是队列调度逻辑里,spi_finalize_current_message之后没有立即检查队列,导致下一个消息要等到下次中断才被处理。加了一个tasklet_schedule就解决了。

4.5 小结:驱动设计的核心思路

sun6i-spi驱动其实不复杂。它的核心思路就两条:

  • 硬件抽象:把所有寄存器操作封装成函数,probe时只做资源申请和初始化。
  • 异步传输:用队列+中断的方式,让CPU在等待SPI传输时可以去干别的事。

这个设计模式在Linux SPI驱动里很常见。你理解了sun6i-spi,再看其他平台的SPI驱动,会发现大同小异。区别主要在FIFO管理方式和DMA支持上。