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_handler | FIFO读写、传输完成、错误处理 |
| 硬件寄存器 | sun6i_spi_regmap 宏定义 | 寄存器偏移、位域定义 |
嗯,这里要注意:驱动里没有用传统的master->transfer回调,而是用了master->transfer_one。这是SPI子系统后来推荐的方式,支持异步传输和队列管理。
sun6i_spi_regmap里的寄存器定义。不同芯片的FIFO深度、时钟分频方式可能有差异,但整体框架几乎不用动。
4.2 probe流程:从设备树到硬件就绪
probe函数是驱动的起点。我把它拆成6个关键步骤,每一步都有坑:
- 获取设备树资源:读取
reg、interrupts、clocks、dmas等属性。T113的SPI控制器有DMA支持,但probe里只是获取DMA通道,实际使用在传输时。 - 映射寄存器地址:
devm_ioremap_resource,把物理地址转成虚拟地址。我遇到过地址重叠导致映射失败的情况,后来发现是设备树里reg写错了范围。 - 时钟使能与复位:
clk_prepare_enable打开SPI模块时钟,reset_control_deassert释放复位。这一步顺序不能反——先时钟后复位,否则硬件可能挂死。 - 配置SPI控制器:设置主模式、关闭中断、清空FIFO。初始状态下的控制器是"干净"的。
- 注册SPI主控:
spi_register_master,把master结构体注册到SPI子系统。注册成功后,系统会自动扫描设备树中的SPI从设备节点。 - 申请中断:
devm_request_irq,注册中断处理函数。注意这里用的是IRQF_SHARED标志,因为有些全志芯片的SPI中断线是共享的。
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链接。流程是这样的:
- 消息入队:
spi_queued_transfer把消息挂到master的队列上,然后调用sun6i_spi_start_transfer尝试启动传输。 - 传输启动:如果当前没有正在进行的传输,就从队列头部取出消息,配置硬件寄存器,写入FIFO,开启中断。
- 中断处理:每次中断处理函数检查FIFO状态,读走接收数据,写入发送数据。如果消息传输完成,调用
spi_finalize_current_message通知上层。 - 队列调度:当前消息完成后,检查队列是否还有待处理消息。有的话,继续启动下一个。
驱动里有个关键函数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会导致睡眠,在中断上下文里不能用。
我画了一张流程图,把整个传输队列的流转关系展示出来:
你想想看,如果队列里同时有5个消息,每个消息包含多个spi_transfer段,驱动必须保证每个段按顺序执行,不能乱。全志的驱动实现里,sun6i_spi_transfer_one只处理一个spi_transfer,完成后通过回调触发下一个。这种设计叫"流水线式处理",简单可靠。
spi_finalize_current_message之后没有立即检查队列,导致下一个消息要等到下次中断才被处理。加了一个tasklet_schedule就解决了。
4.5 小结:驱动设计的核心思路
sun6i-spi驱动其实不复杂。它的核心思路就两条:
- 硬件抽象:把所有寄存器操作封装成函数,probe时只做资源申请和初始化。
- 异步传输:用队列+中断的方式,让CPU在等待SPI传输时可以去干别的事。
这个设计模式在Linux SPI驱动里很常见。你理解了sun6i-spi,再看其他平台的SPI驱动,会发现大同小异。区别主要在FIFO管理方式和DMA支持上。