4、SPI触摸屏驱动:SPI协议基础、SPI设备驱动注册、SPI数据传输与DMA优化

SPI 总线在触摸屏领域用得特别多。为什么?说白了就三个字:快、稳、省。我这些年调过的屏,十块里有七八块都是 SPI 接口。今天咱们就把 SPI 触摸屏驱动这块硬骨头啃下来。

本章核心脉络:先搞懂 SPI 协议怎么握手,再学会怎么在 Linux 里注册一个 SPI 设备,最后用 DMA 把数据传输效率拉满。一条线走到底。

4.1 SPI协议基础——四根线的事儿

SPI 全称 Serial Peripheral Interface,摩托罗拉发明的。它用四根线搞定全双工通信:

  • SCLK(Serial Clock):主设备产生的时钟信号
  • MOSI(Master Out Slave In):主设备发,从设备收
  • MISO(Master In Slave Out):从设备发,主设备收
  • CS(Chip Select):片选,低电平有效

你想想看,这四根线就能同时收发数据,比 I2C 那种半双工效率高多了。我刚开始做驱动时,总觉得 SPI 很简单,结果在时钟极性上栽过跟头——屏就是不亮,折腾了两天才发现是 CPOL 配反了。

4.1.1 四种工作模式

SPI 有四种模式,由 CPOL(时钟极性)和 CPHA(时钟相位)决定:

模式 CPOL CPHA 数据采样边沿 典型场景
模式0 0 0 上升沿采样 最常用,大部分触摸屏默认
模式1 0 1 下降沿采样 部分传感器
模式2 1 0 下降沿采样 少数特定芯片
模式3 1 1 上升沿采样 某些 Flash 存储器

我曾经踩过的坑:某款触摸屏数据手册写的是"Mode 0",结果实际需要 Mode 3 才能正常工作。后来发现是芯片 errata 里有个勘误没看。所以拿到新屏,第一件事就是用逻辑分析仪抓一下实际波形,别全信手册。

4.1.2 数据传输基本单元

SPI 传输以字节为单位。主设备发一个字节,同时从设备回一个字节。这就是所谓的"交换"。触摸屏驱动里,典型的读操作流程是这样的:

// 伪代码:SPI 读触摸坐标
spi_write_then_read(spi_dev, cmd_buf, 1, data_buf, 4);
// 先发一个字节的命令(比如 0x90 读 X 坐标)
// 同时接收 4 个字节的坐标数据

嗯,这里要注意:很多新手以为 SPI 读操作就是"主设备发命令,从设备回数据"。其实不是的。主设备发命令的时候,从设备返回的是无效数据;主设备发 dummy 字节的时候,从设备才返回有效数据。这个"交换"的概念一定要理解透。

4.2 SPI设备驱动注册——让内核认识你的屏

在 Linux 里注册一个 SPI 触摸屏驱动,分两步走:设备树描述 + 驱动代码注册。

4.2.1 设备树配置

我个人习惯先把设备树写好。这样硬件资源一目了然:

// 设备树片段:SPI 触摸屏节点
&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = &spi1_pins;

    touchscreen: touch@0 {
        compatible = "ilitek,ili2511";
        reg = <0>;                 // CS 片选索引
        spi-max-frequency = <10000000>;  // 10MHz
        interrupt-parent = <gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <gpio1 12 GPIO_ACTIVE_LOW>;
    };
};

这里有几个关键点:

  • reg:对应片选号,0 表示 CS0
  • spi-max-frequency:别贪快,先按手册标称值的 80% 设,稳定后再往上提
  • interrupts:触摸屏一般用下降沿触发中断

4.2.2 驱动代码框架

驱动注册的核心是 spi_driver 结构体:

static const struct of_device_id touch_of_match[] = {
    { .compatible = "ilitek,ili2511" },
    { }
};
MODULE_DEVICE_TABLE(of, touch_of_match);

static struct spi_driver touch_driver = {
    .driver = {
        .name = "ili2511_touch",
        .of_match_table = touch_of_match,
    },
    .probe = touch_probe,
    .remove = touch_remove,
};

module_spi_driver(touch_driver);

probe 函数里要做什么?我一般按这个顺序:

  1. 解析设备树,获取 GPIO、中断等资源
  2. 初始化 SPI 设备(设置模式、频率、bits_per_word)
  3. 申请中断
  4. 初始化输入子系统
  5. 启动工作队列或定时器轮询

小技巧:probe 里别做耗时操作。我见过有人直接在 probe 里做第一次通信校验,结果导致系统启动延迟。正确的做法是放到工作队列里异步执行。

4.3 SPI数据传输——从轮询到中断

数据传输是触摸屏驱动的核心。数据传得快不快,直接影响触摸响应速度。

4.3.1 同步传输

最简单的做法,用 spi_write_then_read:

static int touch_read_data(struct spi_device *spi, u8 *buf)
{
    u8 cmd = 0x90;  // 读坐标命令
    struct spi_transfer t = {
        .tx_buf = &cmd,
        .rx_buf = buf,
        .len = 5,   // 1字节命令 + 4字节数据
    };
    struct spi_message m;

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    return spi_sync(spi, &m);
}

这种方式的缺点是:阻塞。每次传输都要等完成才能返回。如果触摸屏采样率是 100Hz,每 10ms 读一次,CPU 就被占死了。

4.3.2 中断触发 + 异步传输

更好的做法:触摸屏有数据时拉中断,在中断处理里启动异步传输:

static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct touch_dev *ts = dev_id;

    // 启动异步 SPI 传输
    spi_async(ts->spi, &ts->msg);
    return IRQ_HANDLED;
}

static void touch_transfer_complete(void *data)
{
    struct touch_dev *ts = data;
    // 解析数据,上报触摸事件
    input_report_key(ts->input, BTN_TOUCH, 1);
    input_sync(ts->input);
}

这样 CPU 不用傻等,中断来了才干活。但问题又来了——如果触摸屏连续上报,中断频率很高,频繁进出中断的开销也不小。

4.4 DMA优化——让数据自己跑

说到 DMA,其实就是把数据传输这件事从 CPU 手里解放出来。CPU 只需要告诉 DMA 控制器:"帮我把这 64 字节从 SPI 搬到这里",然后就可以去干别的事了。

4.4.1 什么时候该用 DMA?

不是所有场景都需要 DMA。我一般按这个标准判断:

传输大小 建议方式 原因
< 16 字节 PIO(编程 I/O) DMA 建立开销大于传输本身
16 ~ 256 字节 视情况而定 看 CPU 负载,负载高就用 DMA
> 256 字节 DMA 必须用,否则 CPU 被拖死

触摸屏每次传输通常只有 4~8 字节,按理说用 PIO 就够了。但如果你做的是多点触控(比如 10 点),每次要读 60 字节以上,DMA 的优势就出来了。

4.4.2 DMA 在 SPI 驱动中的配置

在 Linux 里启用 SPI DMA,主要靠设备树和驱动配合:

// 设备树:使能 SPI 控制器的 DMA 支持
&spi1 {
    dmas = <&edma0 0 2>, <&edma0 1 2>;
    dma-names = "tx", "rx";
    status = "okay";
};

驱动里,用 spi_message 的 is_dma_mapped 标志位:

static int touch_dma_transfer(struct touch_dev *ts)
{
    struct spi_transfer *t = &ts->xfer;

    t->tx_buf = ts->tx_dma_buf;
    t->rx_buf = ts->rx_dma_buf;
    t->len = 64;
    t->tx_dma = ts->tx_dma_handle;
    t->rx_dma = ts->rx_dma_handle;

    spi_message_init(&ts->msg);
    spi_message_add_tail(t, &ts->msg);
    ts->msg.is_dma_mapped = 1;  // 告诉 SPI 核心:用 DMA

    return spi_async(ts->spi, &ts->msg);
}

注意:DMA 缓冲区必须使用 dma_alloc_coherent 或 dma_map_single 分配。不能用栈上的局部变量,也不能用 kmalloc 分配的普通内存。我见过有人直接用全局数组做 DMA 缓冲区,结果 cache 一致性导致数据错乱,查了三天才找到原因。

4.4.3 DMA 性能实测对比

我在某 ARM Cortex-A7 平台上做过对比测试,传输 64 字节数据:

传输方式 CPU 占用率 传输耗时 可同时处理其他任务
PIO 轮询 85% 12 μs 几乎不能
PIO 中断 30% 15 μs 可以
DMA 5% 18 μs 完全没问题

你看,DMA 虽然传输耗时略高(因为 DMA 建立通道有开销),但 CPU 占用率从 85% 降到了 5%。这意味着你的系统可以同时处理 UI 渲染、网络通信等其他任务。

4.5 知识体系总览

下面这张图把 SPI 触摸屏驱动的核心流程串起来了:

SPI触摸屏驱动知识体系 SPI 触摸屏驱动核心流程 SPI 协议基础 4 种工作模式 全双工交换机制 SPI 设备驱动注册 设备树配置 spi_driver 注册 probe 初始化流程 SPI 数据传输 同步传输 (spi_sync) 异步传输 (spi_async) 中断触发传输 DMA 传输 DMA 优化:降低 CPU 占用,提升系统吞吐 核心原则:小数据用 PIO,大数据用 DMA;中断触发代替轮询,异步传输代替同步阻塞

4.6 调试要点总结

最后,把我这些年调 SPI 触摸屏踩过的坑总结一下:

  • 先看波形:逻辑分析仪是必备工具。先确认 SCLK、MOSI、MISO、CS 的时序对不对
  • 模式匹配:CPOL 和 CPHA 必须和从设备一致。错了就是全黑
  • 频率别贪:从标称频率的 50% 开始,稳定了再往上加。我遇到过 20MHz 能跑,25MHz 就丢数据的屏
  • DMA 缓冲区:必须 cache 一致性处理,否则数据错乱别找我
  • 中断优先级:触摸屏中断别设太高,否则可能把系统时钟中断给堵了

我的调试口诀:先看波形后写码,模式频率别搞差。小数据用 PIO,大数据上 DMA。中断触发效率高,异步传输不卡壳。