4、SPI总线驱动开发:SPI核心层API、SPI控制器驱动、SPI设备驱动编写
SPI总线,说白了就是四根线搞定一切通信。SCLK、MOSI、MISO、CS,就这四根。我刚开始做机顶盒驱动时,总觉得SPI比I2C简单多了——没有地址、没有应答、没有复杂的协议栈。但后来发现,越简单的东西,坑往往越深。
这一章,我们重点讲三块内容:SPI核心层API、SPI控制器驱动、SPI设备驱动。这三块搞明白了,你在机顶盒上接什么SPI Flash、SPI触摸屏、SPI射频模块,基本都能手到擒来。
4.1 SPI核心层API:你绕不开的基础设施
Linux内核的SPI子系统,设计得挺巧妙的。它把控制器驱动和设备驱动完全解耦了。中间这一层,就是SPI核心层。核心层提供了一堆API,供上下两层调用。
我个人习惯,先看数据结构,再看操作函数。数据结构是骨架,函数是血肉。
4.1.1 核心数据结构
你写SPI驱动,天天要跟这几个结构体打交道:
| 结构体 | 作用 | 我常用的字段 |
|---|---|---|
struct spi_master |
描述一个SPI控制器 | bus_num、num_chipselect、transfer_one_message |
struct spi_device |
描述一个SPI从设备 | chip_select、max_speed_hz、mode |
struct spi_message |
一次SPI传输的完整描述 | transfers链表、complete回调 |
struct spi_transfer |
一次SPI传输中的一个片段 | tx_buf、rx_buf、len、speed_hz |
嗯,这里要注意:spi_master在较新内核里改名叫spi_controller了。但老内核还是spi_master。我建议你写驱动时,直接看内核版本对应的头文件,别凭记忆写。
4.1.2 核心API速查
核心层提供的API,我按用途分了三类:
- 控制器注册/注销:
spi_register_master()、spi_unregister_master() - 设备注册/注销:
spi_new_device()、spi_alloc_device()、spi_add_device() - 数据传输:
spi_sync()、spi_async()、spi_write_then_read()
我遇到过不少新手,上来就用spi_sync(),结果在中断上下文里调用了,直接内核panic。记住:spi_sync()是阻塞的,只能在进程上下文用。中断里要用spi_async()。
核心原则:SPI核心层帮你管理了总线编号、片选映射、时钟频率协商。你写驱动时,不要自己去操作GPIO模拟片选,除非你明确知道自己在干什么。
4.2 SPI控制器驱动:让硬件动起来
控制器驱动,说白了就是让SoC里的SPI硬件模块能正常工作。机顶盒里常见的SPI控制器,有DesignWare DW_apb_ssi、Freescale DSPI、Rockchip SPI等。但不管哪种,套路都差不多。
4.2.1 控制器驱动的骨架
写一个SPI控制器驱动,你需要实现这几个回调:
static int spi_master_probe(struct platform_device *pdev)
{
struct spi_master *master;
struct my_spi_data *spi_data;
// 1. 分配spi_master
master = spi_alloc_master(&pdev->dev, sizeof(*spi_data));
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;
// 3. 实现传输回调
master->transfer_one_message = my_transfer_one_message;
master->setup = my_setup;
// 4. 注册master
spi_register_master(master);
return 0;
}
这里有个坑,我踩过好几次。spi_alloc_master()的第二个参数是私有数据的大小。这个私有数据会紧跟在spi_master结构体后面分配。用spi_master_get_devdata()来获取。千万别自己单独malloc一个结构体,然后挂在master的某个字段上——那样生命周期管理会乱套。
4.2.2 传输回调的实现
transfer_one_message是控制器驱动的核心。它负责把spi_message里的所有spi_transfer逐个发出去。
我曾经在一个机顶盒项目里,遇到SPI传输偶尔丢数据的问题。查了两天,最后发现是FIFO阈值设置不对。硬件FIFO深度是16字节,我设的阈值是8,结果发送时FIFO空了,时钟还在跑,就收到了垃圾数据。
避坑指南:我曾经在写SPI控制器驱动时,忽略了spi_transfer里的bits_per_word字段。默认是8位,但有个SPI Flash需要16位模式。结果读出来的ID全是错的。后来加上master->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16);才搞定。
4.3 SPI设备驱动:跟外设打交道
设备驱动,就是具体跟某个SPI外设通信的代码。比如SPI Flash、SPI触摸屏、SPI音频Codec。这部分工作,说白了就是拼凑spi_message,然后发出去。
4.3.1 设备驱动的注册
SPI设备驱动通过spi_driver结构体注册:
static struct spi_driver my_spi_device_driver = {
.driver = {
.name = "my_spi_device",
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
.id_table = my_spi_id_table,
};
module_spi_driver(my_spi_device_driver);
嗯,这里要注意:id_table和of_match_table二选一就行。现在机顶盒基本都是设备树了,所以of_match_table用得更多。
4.3.2 数据传输的几种姿势
我总结了三种常用的传输方式:
- 同步传输:
spi_sync(),简单粗暴,但会阻塞。适合在进程上下文用。 - 异步传输:
spi_async(),不阻塞,通过回调通知完成。适合在中断上下文用。 - 写后读:
spi_write_then_read(),专门为SPI Flash这类设备设计的——先发命令,再读数据。
举个例子,读一个SPI Flash的ID:
static int read_spi_flash_id(struct spi_device *spi)
{
u8 cmd = 0x9F; // 读ID命令
u8 id[3];
int ret;
// 方式一:用spi_write_then_read
ret = spi_write_then_read(spi, &cmd, 1, id, 3);
if (ret < 0)
dev_err(&spi->dev, "read id failed\n");
// 方式二:手动构造spi_message
struct spi_message msg;
struct spi_transfer t = {
.tx_buf = &cmd,
.rx_buf = id,
.len = 4, // 先发1字节命令,再收3字节数据
};
spi_message_init(&msg);
spi_message_add_tail(&t, &msg);
ret = spi_sync(spi, &msg);
return (id[0] << 16) | (id[1] << 8) | id[2];
}
我个人更推荐spi_write_then_read(),因为它内部帮你处理了片选信号的保持问题。你想想看,如果自己构造message,片选在命令发完后就释放了,再读数据时片选又拉低一次——有些SPI Flash会认为这是新的命令序列,直接罢工。
4.3.3 设备树配置
机顶盒里,SPI设备的信息通常来自设备树。一个典型的SPI设备节点长这样:
&spi0 {
status = "okay";
cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
spi-cpol;
spi-cpha;
};
};
这里有个细节:cs-gpios是控制器级别的属性,指定了片选用的GPIO。而reg = <0>表示这个设备使用第0个片选。我见过有人把这两个搞混,结果片选信号死活拉不下来。
经验之谈:调试SPI设备驱动时,先用逻辑分析仪抓波形。看片选有没有拉低、时钟有没有输出、数据线有没有电平变化。我80%的SPI问题,都是靠逻辑分析仪定位的。别一上来就怀疑代码逻辑。
4.4 本章小结
SPI总线驱动开发,其实就三件事:
- 核心层API:记住那几个结构体和函数,别在中断里调
spi_sync()。 - 控制器驱动:实现
transfer_one_message回调,注意FIFO阈值和位宽设置。 - 设备驱动:拼好
spi_message,用spi_write_then_read()简化操作。
下一章,我们会讲I2C总线驱动开发。I2C比SPI多了一个地址和应答机制,但核心思路是一样的——理解核心层,写好控制器和设备驱动。
嗯,今天就到这里。有什么问题,欢迎交流。