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 函数里要做什么?我一般按这个顺序:
- 解析设备树,获取 GPIO、中断等资源
- 初始化 SPI 设备(设置模式、频率、bits_per_word)
- 申请中断
- 初始化输入子系统
- 启动工作队列或定时器轮询
小技巧: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 触摸屏驱动的核心流程串起来了:
4.6 调试要点总结
最后,把我这些年调 SPI 触摸屏踩过的坑总结一下:
- 先看波形:逻辑分析仪是必备工具。先确认 SCLK、MOSI、MISO、CS 的时序对不对
- 模式匹配:CPOL 和 CPHA 必须和从设备一致。错了就是全黑
- 频率别贪:从标称频率的 50% 开始,稳定了再往上加。我遇到过 20MHz 能跑,25MHz 就丢数据的屏
- DMA 缓冲区:必须 cache 一致性处理,否则数据错乱别找我
- 中断优先级:触摸屏中断别设太高,否则可能把系统时钟中断给堵了
我的调试口诀:先看波形后写码,模式频率别搞差。小数据用 PIO,大数据上 DMA。中断触发效率高,异步传输不卡壳。